feat: create basic front end

This commit is contained in:
Muhsin Keloth
2024-05-17 12:06:19 +05:30
parent e992c23d03
commit 0e11dac471
12 changed files with 157 additions and 69 deletions
@@ -23,18 +23,6 @@
:key="element.name"
class="bg-white dark:bg-gray-800"
>
<div
v-if="element.name === 'linear' && isLinearIntegrationEnabled"
class="conversation--actions"
>
<accordion-item
title="Linear"
:is-open="isContactSidebarItemOpen('is_linear_open')"
@click="value => toggleSidebarUIState('is_linear_open', value)"
>
<linear :conversation-id="conversationId" />
</accordion-item>
</div>
<div
v-if="element.name === 'conversation_actions'"
class="conversation--actions"
@@ -157,8 +145,6 @@ import CustomAttributes from './customAttributes/CustomAttributes.vue';
import draggable from 'vuedraggable';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import MacrosList from './Macros/List.vue';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import Linear from './linear/index.vue';
export default {
components: {
AccordionItem,
@@ -170,7 +156,6 @@ export default {
ConversationParticipant,
draggable,
MacrosList,
Linear,
},
mixins: [alertMixin, uiSettingsMixin],
props: {
@@ -226,12 +211,6 @@ export default {
const { custom_attributes: customAttributes } = this.contact;
return customAttributes && Object.keys(customAttributes).length;
},
isLinearIntegrationEnabled() {
return this.isFeatureEnabledonAccount(
this.accountId,
FEATURE_FLAGS.LINEAR
);
},
},
watch: {
conversationId(newConversationId, prevConversationId) {
@@ -1,286 +0,0 @@
<template>
<div @submit.prevent="onSubmit">
<woot-input
v-model="title"
:class="{ error: $v.title.$error }"
class="w-full"
:styles="{ ...inputStyles, padding: '6px 12px' }"
:label="$t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.TITLE.LABEL')"
:placeholder="
$t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.TITLE.PLACEHOLDER')
"
:error="
$v.title.$error
? $t(
'INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.TITLE.REQUIRED_ERROR'
)
: ''
"
@input="$v.title.$touch"
/>
<label>
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.DESCRIPTION.LABEL') }}
<textarea
v-model="description"
:style="{ ...inputStyles, padding: '8px 12px' }"
rows="3"
type="text"
class="text-sm"
:placeholder="
$t(
'INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.DESCRIPTION.PLACEHOLDER'
)
"
/>
</label>
<label :class="{ error: $v.teamId.$error }">
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.TEAM.LABEL') }}
<select
v-model="teamId"
:style="inputStyles"
@change="onChangeTeam($event)"
>
<option v-for="item in teams" :key="item.name" :value="item.id">
{{ item.name }}
</option>
</select>
<span v-if="$v.teamId.$error" class="message">
{{
$t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.TEAM.REQUIRED_ERROR')
}}
</span>
</label>
<label>
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.ASSIGNEE.LABEL') }}
<select v-model="assigneeId" :style="inputStyles">
<option v-for="item in assignees" :key="item.name" :value="item.id">
{{ item.name }}
</option>
</select>
</label>
<label>
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.LABEL.LABEL') }}
<select v-model="labelId" :style="inputStyles">
<option v-for="item in labels" :key="item.name" :value="item.id">
{{ item.name }}
</option>
</select>
</label>
<label>
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.PRIORITY.LABEL') }}
<select v-model="priority" :style="inputStyles">
<option v-for="item in priorities" :key="item.name" :value="item.id">
{{ item.name }}
</option>
</select>
</label>
<label>
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.PROJECT.LABEL') }}
<select v-model="projectId" :style="inputStyles">
<option v-for="item in projects" :key="item.name" :value="item.id">
{{ item.name }}
</option>
</select>
</label>
<label>
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.STATUS.LABEL') }}
<select v-model="stateId" :style="inputStyles">
<option v-for="item in statuses" :key="item.name" :value="item.id">
{{ item.name }}
</option>
</select>
</label>
<div class="flex items-center justify-end w-full gap-2 mt-8">
<woot-button
class="px-4 rounded-xl button clear outline-woot-200/50 outline"
@click.prevent="onClose"
>
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CANCEL') }}
</woot-button>
<woot-button
:is-disabled="isSubmitDisabled"
class="px-4 rounded-xl"
:is-loading="isCreating"
@click.prevent="createIssue"
>
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CREATE') }}
</woot-button>
</div>
</div>
</template>
<script>
import alertMixin from 'shared/mixins/alertMixin';
import LinearAPI from 'dashboard/api/integrations/linear';
import validations from './validations';
export default {
mixins: [alertMixin],
props: {
accountId: {
type: [Number, String],
required: true,
},
conversationId: {
type: [Number, String],
required: true,
},
},
data() {
return {
title: '',
description: '',
teamId: '',
assigneeId: '',
projectId: '',
stateId: '',
labelId: '',
priority: 0,
teams: [],
assignees: [],
projects: [],
labels: [],
statuses: [],
selectedTabIndex: 0,
priorities: [
{
id: 0,
name: 'No priority',
},
{
id: 1,
name: 'Urgent',
},
{
id: 2,
name: 'High',
},
{
id: 3,
name: 'Normal',
},
{
id: 4,
name: 'Low',
},
],
isCreating: false,
searchResults: [
{
id: 1,
title: 'Test',
},
],
inputStyles: {
borderRadius: '12px',
fontSize: '14px',
},
};
},
validations,
computed: {
isSubmitDisabled() {
return this.$v.title.$invalid || this.isCreating;
},
},
mounted() {
this.getTeams();
},
methods: {
onClose() {
this.$emit('close');
},
onClickTabChange(index) {
this.selectedTabIndex = index;
},
onChangeTeam(event) {
this.teamId = event.target.value;
this.assigneeId = '';
this.stateId = '';
this.labelId = '';
this.getTeamEntities();
},
async getTeams() {
try {
const response = await LinearAPI.getTeams();
this.teams = response.data;
} catch (error) {
this.showAlert(
this.$t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.LOADING_TEAM_ERROR')
);
} finally {
this.isLoading = false;
}
},
async getTeamEntities() {
try {
const response = await LinearAPI.getTeamEntities(this.teamId);
const { users, labels, projects, states } = response.data;
this.assignees = users;
this.labels = labels;
this.statuses = states;
this.projects = projects;
} catch (error) {
this.showAlert(
this.$t(
'INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.LOADING_TEAM_ENTITIES_ERROR'
)
);
} finally {
this.isLoading = false;
}
},
async createIssue() {
this.$v.$touch();
if (this.$v.$invalid) {
return;
}
const payload = {
team_id: this.teamId,
title: this.title,
};
if (this.description) {
payload.description = this.description;
}
if (this.assigneeId) {
payload.assignee_id = this.assigneeId;
}
if (this.projectId) {
payload.project_id = this.projectId;
}
if (this.stateId) {
payload.state_id = this.stateId;
}
if (this.priority) {
payload.priority = this.priority;
}
if (this.labelId) {
payload.label_ids = [this.labelId];
}
try {
this.isCreating = true;
const response = await LinearAPI.createIssue(payload);
const { id: issueId } = response.data;
await LinearAPI.link_issue(this.conversationId, issueId);
this.showAlert(
this.$t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CREATE_SUCCESS')
);
this.onClose();
} catch (error) {
this.showAlert(
this.$t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CREATE_ERROR')
);
} finally {
this.isCreating = false;
}
},
handleAgentsFilterSelection() {},
},
};
</script>
@@ -1,92 +0,0 @@
<template>
<div class="flex flex-col h-auto overflow-auto">
<woot-modal-header
:header-title="$t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.TITLE')"
:header-content="
$t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.DESCRIPTION')
"
/>
<div class="flex flex-col h-auto overflow-auto">
<div class="flex flex-col px-8 pb-4">
<woot-tabs
class="ltr:[&>ul]:pl-0 rtl:[&>ul]:pr-0"
:index="selectedTabIndex"
@change="onClickTabChange"
>
<woot-tabs-item
v-for="tab in tabs"
:key="tab.key"
:name="tab.name"
:show-badge="false"
/>
</woot-tabs>
</div>
<div v-if="selectedTabIndex === 0" class="flex flex-col px-8 pb-4">
<create-issue
:account-id="accountId"
:conversation-id="conversationId"
@close="onClose"
/>
</div>
<div v-else class="flex flex-col px-8 pb-4">
<link-issue :conversation-id="conversationId" @close="onClose" />
</div>
</div>
</div>
</template>
<script>
import alertMixin from 'shared/mixins/alertMixin';
import LinkIssue from './LinkIssue';
import CreateIssue from './CreateIssue';
import validations from './validations';
export default {
components: {
LinkIssue,
CreateIssue,
},
mixins: [alertMixin],
props: {
accountId: {
type: [Number, String],
required: true,
},
conversationId: {
type: [Number, String],
required: true,
},
},
data() {
return {
selectedTabIndex: 0,
};
},
validations,
computed: {
tabs() {
return [
{
key: 0,
name: this.$t('INTEGRATION_SETTINGS.LINEAR.CREATE'),
},
{
key: 1,
name: this.$t('INTEGRATION_SETTINGS.LINEAR.LINK.TITLE'),
},
];
},
},
methods: {
onClose() {
this.$emit('close');
},
onClickTabChange(index) {
this.selectedTabIndex = index;
},
},
};
</script>
@@ -1,144 +0,0 @@
<template>
<div class="flex flex-col gap-2">
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between group">
<a
:href="issue.url"
target="_blank"
rel="noopener noreferrer"
class="inline-block rounded-sm mb-0 break-all py-0.5 text-primary-600"
>
{{ `${issue.identifier} ${issue.title}` }}
</a>
<woot-button
size="small"
variant="clear"
color-scheme="secondary"
class="!px-2 hover:!bg-transparent hidden dark:hover:!bg-transparent underline group-hover:block"
@click="unlinkIssue"
>
{{ $t('INTEGRATION_SETTINGS.LINEAR.UNLINK.TITLE') }}
</woot-button>
</div>
<span class="text-sm font-normal text-slate-900 dark:text-slate-25">
{{ issue.description }}
</span>
</div>
<div class="flex justify-between w-full">
<span
class="text-sm sticky top-0 h-fit font-normal tracking-[-0.6%] min-w-[140px] truncate text-slate-600 dark:text-slate-200"
>
{{ $t('INTEGRATION_SETTINGS.LINEAR.ISSUE.STATUS') }}
</span>
<div class="flex flex-col w-full gap-2">
<span
class="text-sm font-normal text-left text-slate-900 dark:text-slate-25 tabular-nums"
>
{{ issue.state.name }}
</span>
</div>
</div>
<div class="flex justify-between w-full">
<span
class="text-sm sticky top-0 h-fit font-normal tracking-[-0.6%] min-w-[140px] truncate text-slate-600 dark:text-slate-200"
>
{{ $t('INTEGRATION_SETTINGS.LINEAR.ISSUE.PRIORITY') }}
</span>
<div class="flex flex-col w-full gap-2">
<span
class="text-sm font-normal text-left text-slate-900 dark:text-slate-25 tabular-nums"
>
{{ priorityLabel }}
</span>
</div>
</div>
<div class="flex justify-between w-full">
<span
class="text-sm sticky top-0 h-fit font-normal tracking-[-0.6%] min-w-[140px] truncate text-slate-600 dark:text-slate-200"
>
{{ $t('INTEGRATION_SETTINGS.LINEAR.ISSUE.ASSIGNEE') }}
</span>
<div class="flex flex-col w-full gap-2">
<span
class="text-sm font-normal text-left text-slate-900 dark:text-slate-25 tabular-nums"
>
{{ assigneeName }}
</span>
</div>
</div>
<div class="flex justify-between w-full">
<span
class="text-sm sticky top-0 h-fit font-normal tracking-[-0.6%] min-w-[140px] truncate text-slate-600 dark:text-slate-200"
>
{{ $t('INTEGRATION_SETTINGS.LINEAR.ISSUE.LABELS') }}
</span>
<div class="flex flex-col w-full gap-2">
<span
class="text-sm font-normal text-left text-slate-900 dark:text-slate-25 tabular-nums"
>
{{ labels }}
</span>
</div>
</div>
<div class="flex justify-between w-full">
<span
class="text-sm sticky top-0 h-fit font-normal tracking-[-0.6%] min-w-[140px] truncate text-slate-600 dark:text-slate-200"
>
{{ $t('INTEGRATION_SETTINGS.LINEAR.ISSUE.CREATED_AT') }}
</span>
<div class="flex flex-col w-full gap-2">
<span
class="text-sm font-normal text-left text-slate-900 dark:text-slate-25 tabular-nums"
>
{{ formatDate(issue.createdAt) }}
</span>
</div>
</div>
</div>
</template>
<script>
import { format } from 'date-fns';
const priorityMap = {
0: 'Low',
1: 'Medium',
2: 'High',
3: 'Urgent',
};
export default {
props: {
issue: {
type: Object,
required: true,
},
linkId: {
type: String,
required: true,
},
},
computed: {
priorityLabel() {
return priorityMap[this.issue.priority];
},
assigneeName() {
return this.issue.assignee ? this.issue.assignee.name : '---';
},
labels() {
return this.issue.labels.nodes.length
? this.issue.labels.nodes.map(label => label.name).join(', ')
: '---';
},
},
methods: {
unlinkIssue() {
this.$emit('unlink-issue', this.linkId);
},
formatDate(timestamp) {
return format(new Date(timestamp), 'MMM dd, hh:mm a');
},
},
};
</script>
@@ -1,126 +0,0 @@
<template>
<div>
<div class="multiselect-wrap--small">
<multiselect
v-model="selectedOptions"
class="no-margin !pl-0"
:placeholder="$t('INTEGRATION_SETTINGS.LINEAR.LINK.SEARCH')"
:select-label="$t('INTEGRATION_SETTINGS.LINEAR.LINK.SELECT')"
label="title"
track-by="id"
:options="options"
:option-height="24"
:show-labels="false"
:max-height="500"
@search-change="handleSearchChange"
@select="handleInput"
>
<template #noResult>
<div class="flex items-center justify-center">
{{ emptyText }}
</div>
</template>
<template #noOptions>
<div class="flex items-center justify-center">
{{ $t('INTEGRATION_SETTINGS.LINEAR.LINK.EMPTY_LIST') }}
</div>
</template>
</multiselect>
</div>
<div class="flex items-center justify-end w-full gap-2 mt-8">
<woot-button
class="px-4 rounded-xl button clear outline-woot-200/50 outline"
@click.prevent="onClose"
>
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CANCEL') }}
</woot-button>
<woot-button
:is-disabled="isSubmitDisabled"
class="px-4 rounded-xl"
:is-loading="isLinking"
@click.prevent="linkIssue"
>
{{ $t('INTEGRATION_SETTINGS.LINEAR.LINK.TITLE') }}
</woot-button>
</div>
</div>
</template>
<script>
import alertMixin from 'shared/mixins/alertMixin';
import LinearAPI from 'dashboard/api/integrations/linear';
export default {
mixins: [alertMixin],
props: {
conversationId: {
type: [Number, String],
required: true,
},
},
data() {
return {
selectedOptions: null,
options: [],
inputStyles: {
borderRadius: '12px',
padding: '6px 12px',
fontSize: '14px',
},
isFetching: false,
isLinking: false,
searchQuery: '',
};
},
computed: {
emptyText() {
if (this.isFetching) {
return this.$t('INTEGRATION_SETTINGS.LINEAR.LINK.LOADING');
}
if (this.searchQuery) {
return '';
}
return this.$t('INTEGRATION_SETTINGS.LINEAR.LINK.EMPTY_LIST');
},
isSubmitDisabled() {
return !this.selectedOptions || this.isLinking;
},
},
methods: {
onClose() {
this.$emit('close');
},
async handleSearchChange(value) {
if (!value) return;
this.searchQuery = value;
try {
this.isFetching = true;
const response = await LinearAPI.searchIssues(value);
this.options = response.data;
} catch (error) {
this.showAlert(this.$t('INTEGRATION_SETTINGS.LINEAR.LINK.ERROR'));
} finally {
this.isFetching = false;
}
},
handleInput() {
this.$emit('agents-filter-selection', this.selectedOptions);
},
async linkIssue() {
const { id: issueId } = this.selectedOptions;
try {
this.isLinking = true;
await LinearAPI.link_issue(this.conversationId, issueId);
this.showAlert(
this.$t('INTEGRATION_SETTINGS.LINEAR.LINK.LINK_SUCCESS')
);
this.searchQuery = '';
this.onClose();
} catch (error) {
this.showAlert(this.$t('INTEGRATION_SETTINGS.LINEAR.LINK.LINK_ERROR'));
} finally {
this.isLinking = false;
}
},
},
};
</script>
@@ -1,122 +0,0 @@
<template>
<div class="flex flex-col">
<div class="flex flex-row gap-2">
<woot-button
v-if="shouldShowAddButton"
variant="smooth"
icon="add"
size="tiny"
@click="showCreateIssuePopup"
>
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK_BUTTON') }}
</woot-button>
</div>
<div v-if="isFetching" class="flex items-center justify-center">
<span class="text-sm font-medium text-slate-800 dark:text-slate-100">
{{ $t('INTEGRATION_SETTINGS.LINEAR.LOADING') }}
</span>
</div>
<div v-if="shouldShowItem" class="flex flex-col gap-2">
<issue-item
:issue="linkedIssue.issue"
:link-id="linkedIssue.id"
:conversation-id="conversationId"
@unlink-issue="unlinkIssue"
/>
</div>
<woot-modal
:show.sync="showPopup"
:on-close="closePopup"
class="!items-start [&>div]:!top-12"
>
<create-or-link-issue
:conversation-id="conversationId"
:account-id="currentAccountId"
@close="closePopup"
/>
</woot-modal>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import IssueItem from './IssueItem.vue';
import accountMixin from 'dashboard/mixins/account.js';
import alertMixin from 'shared/mixins/alertMixin';
import CreateOrLinkIssue from './CreateOrLinkIssue.vue';
import LinearAPI from 'dashboard/api/integrations/linear';
export default {
components: {
IssueItem,
CreateOrLinkIssue,
},
mixins: [accountMixin, alertMixin],
props: {
conversationId: {
type: [Number, String],
required: true,
},
},
data() {
return {
isFetching: false,
showPopup: false,
linkedIssue: null,
};
},
computed: {
...mapGetters({
currentAccountId: 'getCurrentAccountId',
}),
shouldShowAddButton() {
return !this.isFetching && !this.linkedIssue;
},
shouldShowItem() {
return !this.isFetching && this.linkedIssue;
},
},
watch: {
conversationId(newConversationId, prevConversationId) {
if (newConversationId && newConversationId !== prevConversationId) {
this.loadLinkedIssue();
}
},
},
mounted() {
this.loadLinkedIssue();
},
methods: {
showCreateIssuePopup() {
this.showPopup = true;
},
closePopup() {
this.showPopup = false;
this.loadLinkedIssue();
},
async loadLinkedIssue() {
try {
this.isFetching = true;
const response = await LinearAPI.getLinkedIssue(this.conversationId);
const issues = response.data;
this.linkedIssue = issues && issues.length ? issues[0] : null;
} catch (error) {
this.showAlert(this.$t('INTEGRATION_SETTINGS.LINEAR.LOADING_ERROR'));
} finally {
this.isFetching = false;
}
},
async unlinkIssue(linkId) {
try {
await LinearAPI.unlinkIssue(linkId);
this.linkedIssue = null;
this.showAlert(this.$t('INTEGRATION_SETTINGS.LINEAR.UNLINK.SUCCESS'));
} catch (error) {
this.showAlert(
this.$t('INTEGRATION_SETTINGS.LINEAR.UNLINK.DELETE_ERROR')
);
}
},
},
};
</script>
@@ -1,10 +0,0 @@
import { required } from 'vuelidate/lib/validators';
export default {
title: {
required,
},
teamId: {
required,
},
};