feat: create basic front end
This commit is contained in:
@@ -71,6 +71,10 @@
|
||||
:class="{ 'justify-end': isContactPanelOpen }"
|
||||
>
|
||||
<SLA-card-label v-if="hasSlaPolicyId" :chat="chat" show-extended-info />
|
||||
<linear
|
||||
v-if="isLinearIntegrationEnabled"
|
||||
:conversation-id="currentChat.id"
|
||||
/>
|
||||
<more-actions :conversation-id="currentChat.id" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -89,6 +93,8 @@ import SLACardLabel from './components/SLACardLabel.vue';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { conversationListPageURL } from 'dashboard/helper/URLHelper';
|
||||
import { snoozedReopenTime } from 'dashboard/helper/snoozeHelpers';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import Linear from './linear/index.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -97,6 +103,7 @@ export default {
|
||||
MoreActions,
|
||||
Thumbnail,
|
||||
SLACardLabel,
|
||||
Linear,
|
||||
},
|
||||
mixins: [inboxMixin, agentMixin, keyboardEventListenerMixins],
|
||||
props: {
|
||||
@@ -121,6 +128,8 @@ export default {
|
||||
...mapGetters({
|
||||
uiFlags: 'inboxAssignableAgents/getUIFlags',
|
||||
currentChat: 'getSelectedChat',
|
||||
accountId: 'getCurrentAccountId',
|
||||
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||
}),
|
||||
chatMetadata() {
|
||||
return this.chat.meta;
|
||||
@@ -178,6 +187,12 @@ export default {
|
||||
hasSlaPolicyId() {
|
||||
return this.chat?.sla_policy_id;
|
||||
},
|
||||
isLinearIntegrationEnabled() {
|
||||
return this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
FEATURE_FLAGS.LINEAR
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
<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>
|
||||
@@ -0,0 +1,92 @@
|
||||
<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>
|
||||
@@ -0,0 +1,144 @@
|
||||
<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>
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
<script setup>
|
||||
import { format } from 'date-fns';
|
||||
import UserAvatarWithName from 'dashboard/components/widgets/UserAvatarWithName.vue';
|
||||
import { computed } from 'vue';
|
||||
const priorityMap = {
|
||||
0: 'Low',
|
||||
1: 'Medium',
|
||||
2: 'High',
|
||||
3: 'Urgent',
|
||||
};
|
||||
const props = defineProps({
|
||||
issue: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const formattedDate = computed(() => {
|
||||
return format(new Date(props.issue.createdAt), 'hh:mm a, MMM dd');
|
||||
});
|
||||
|
||||
const assignee = computed(() => {
|
||||
if (!props.issue.assignee) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
name: props.issue.assignee.name,
|
||||
thumbnail: props.issue.assignee.avatarUrl,
|
||||
};
|
||||
});
|
||||
|
||||
const labels = computed(() => {
|
||||
if (props.issue.labels.nodes.length) {
|
||||
return props.issue.labels.nodes;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
const priorityLabel = computed(() => {
|
||||
return priorityMap[props.issue.priority];
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="absolute flex flex-col items-start bg-[#fdfdfd] dark:bg-slate-800 z-50 p-4 border border-solid border-slate-75 dark:border-slate-700 w-[384px] rounded-xl gap-4 max-h-96 overflow-auto"
|
||||
>
|
||||
<div class="flex flex-col items-start gap-2">
|
||||
<div
|
||||
class="flex items-center justify-center gap-1 px-2 py-1 font-medium border rounded-lg border-ash-200"
|
||||
>
|
||||
<fluent-icon
|
||||
icon="linear"
|
||||
size="19"
|
||||
class="text-[#5E6AD2]"
|
||||
view-box="0 0 19 19"
|
||||
/>
|
||||
<span class="text-xs font-medium text-ash-900">
|
||||
{{ issue.identifier }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-ash-900">
|
||||
{{ issue.title }}
|
||||
</span>
|
||||
<span class="text-sm text-ash-800">
|
||||
{{
|
||||
issue.description.length > 130
|
||||
? issue.description.slice(0, 130) + '...'
|
||||
: issue.description
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 divide-x divide-ash-200">
|
||||
<user-avatar-with-name v-if="assignee" :user="assignee" />
|
||||
<div class="flex items-center gap-1 px-2">
|
||||
<fluent-icon icon="status" size="14" />
|
||||
<h6 class="my-0 text-xs text-slate-600">{{ issue.state.name }}</h6>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 px-2">
|
||||
<fluent-icon icon="priority-high" size="14" view-box="0 0 14 14" />
|
||||
<h6 class="my-0 text-xs text-slate-600">{{ priorityLabel }}</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="labels.length" class="flex items-center gap-1">
|
||||
<woot-label
|
||||
v-for="label in labels"
|
||||
:key="label.id"
|
||||
:title="label.name"
|
||||
:description="label.description"
|
||||
:color="label.color"
|
||||
variant="smooth"
|
||||
small
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<span class="text-xs text-ash-800">
|
||||
{{ `Created at ${formattedDate}` }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,126 @@
|
||||
<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>
|
||||
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div class="relative" @mouseover="openSlaPopover()">
|
||||
<woot-button
|
||||
v-on-clickaway="closeSlaPopover"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
@click="openSlaPopover()"
|
||||
>
|
||||
<fluent-icon
|
||||
icon="linear"
|
||||
size="19"
|
||||
class="text-[#5E6AD2]"
|
||||
view-box="0 0 19 19"
|
||||
/>
|
||||
</woot-button>
|
||||
<linear-issue-details
|
||||
v-if="showSlaPopoverCard"
|
||||
:issue="linkedIssue.issue"
|
||||
class="absolute right-0 top-[46px]"
|
||||
/>
|
||||
<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 LinearIssueDetails from './LinearIssueDetails.vue';
|
||||
import LinearAPI from 'dashboard/api/integrations/linear';
|
||||
import CreateOrLinkIssue from './CreateOrLinkIssue.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LinearIssueDetails,
|
||||
CreateOrLinkIssue,
|
||||
},
|
||||
props: {
|
||||
conversationId: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
linkedIssue: null,
|
||||
showSlaPopover: false,
|
||||
showPopup: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentAccountId: 'getCurrentAccountId',
|
||||
}),
|
||||
showSlaPopoverCard() {
|
||||
return this.showSlaPopover && this.linkedIssue;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
conversationId(newConversationId, prevConversationId) {
|
||||
if (newConversationId && newConversationId !== prevConversationId) {
|
||||
this.loadLinkedIssue();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadLinkedIssue();
|
||||
},
|
||||
methods: {
|
||||
closePopup() {
|
||||
this.showPopup = false;
|
||||
this.loadLinkedIssue();
|
||||
},
|
||||
async loadLinkedIssue() {
|
||||
try {
|
||||
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'));
|
||||
}
|
||||
},
|
||||
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')
|
||||
);
|
||||
}
|
||||
},
|
||||
openSlaPopover() {
|
||||
if (!this.linkedIssue) this.showPopup = true;
|
||||
this.showSlaPopover = true;
|
||||
},
|
||||
closeSlaPopover() {
|
||||
this.showSlaPopover = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,10 @@
|
||||
import { required } from 'vuelidate/lib/validators';
|
||||
|
||||
export default {
|
||||
title: {
|
||||
required,
|
||||
},
|
||||
teamId: {
|
||||
required,
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user