Compare commits

...
Author SHA1 Message Date
Muhsin Keloth 935b394fb5 Update integrations.json 2024-05-23 12:48:53 +05:30
Muhsin Keloth a5c8c790e9 chore: add conversation id 2024-05-23 12:46:50 +05:30
Muhsin Keloth a95ea1a5a4 feat: add title for linear issue links 2024-05-23 09:45:32 +05:30
Muhsin Keloth 4a692e7e3a Merge branch 'feature/cw-3271' of https://github.com/chatwoot/chatwoot into feature/cw-3271 2024-05-23 09:10:50 +05:30
Muhsin Keloth 74dc1ef2ae fix: project id 2024-05-23 09:08:40 +05:30
Muhsin KelothandGitHub 723763b54d Merge branch 'develop' into feature/cw-3271 2024-05-23 09:08:23 +05:30
Muhsin Keloth a4ec09a744 chore: review fixes 2024-05-22 16:01:21 +05:30
Muhsin Keloth e847c34380 chore: review fixes 2024-05-22 15:03:10 +05:30
Sivin VargheseandGitHub 023d3e0821 Merge branch 'develop' into feature/cw-3271 2024-05-22 14:07:32 +05:30
Muhsin KelothandGitHub 63e407b1c5 Merge branch 'feat/linear-backend' into feature/cw-3271 2024-05-22 13:01:00 +05:30
Muhsin Keloth 717e744b08 Update linear.js 2024-05-22 13:00:50 +05:30
Muhsin Keloth aefe3a1b58 chore: rename linked_issues route 2024-05-22 12:57:53 +05:30
Muhsin KelothandGitHub eab8d8d0fb Merge branch 'feat/linear-backend' into feature/cw-3271 2024-05-22 12:13:41 +05:30
Muhsin Keloth dd78655181 chore: review fixes 2024-05-22 11:59:40 +05:30
Muhsin KelothandGitHub 831ac02d82 Merge branch 'feat/linear-backend' into feature/cw-3271 2024-05-22 08:56:18 +05:30
Muhsin KelothandGitHub a0d85557e4 Merge branch 'develop' into feat/linear-backend 2024-05-22 08:54:18 +05:30
Muhsin KelothandGitHub fb22659ec8 Merge branch 'feat/linear-backend' into feature/cw-3271 2024-05-21 19:51:12 +05:30
Muhsin Keloth 23ac2d9e4b chore: remove unused code 2024-05-21 16:53:20 +05:30
Muhsin Keloth 497471632b Merge branch 'feat/linear-backend' of https://github.com/chatwoot/chatwoot into feat/linear-backend 2024-05-21 13:50:58 +05:30
Muhsin Keloth 903ec124f6 chore: add identifier in search query 2024-05-21 13:50:48 +05:30
Muhsin Keloth 902f3aa926 Merge branch 'feature/cw-3271' of https://github.com/chatwoot/chatwoot into feature/cw-3271 2024-05-21 13:50:40 +05:30
Muhsin Keloth 4b0345a577 chore: remove unwanted changes 2024-05-21 13:50:31 +05:30
Muhsin Keloth a2dab6a3a8 chore: remove unwanted changes 2024-05-21 12:42:07 +05:30
Muhsin KelothandGitHub d278107133 Merge branch 'feat/linear-backend' into feature/cw-3271 2024-05-21 12:37:31 +05:30
Muhsin Keloth efb18c1b64 chore: add identifier in search query 2024-05-21 12:37:21 +05:30
Muhsin KelothandGitHub 4c86d56200 Merge branch 'feat/linear-backend' into feature/cw-3271 2024-05-21 12:36:44 +05:30
Muhsin Keloth 70e1bf8d31 chore: cleanup 2024-05-21 12:36:19 +05:30
Muhsin Keloth e8f5e35dc9 chore: add title in link issue 2024-05-21 12:34:05 +05:30
Muhsin Keloth df0a7a348b chore: change link dropdown component 2024-05-20 17:05:07 +05:30
Muhsin KelothandGitHub 6550287f97 Merge branch 'feat/linear-backend' into feature/cw-3271 2024-05-20 16:25:30 +05:30
Muhsin KelothandGitHub cfb1ae0dd1 Merge branch 'develop' into feat/linear-backend 2024-05-20 16:25:23 +05:30
Muhsin Keloth 95d2a57a81 chore: add create comment api 2024-05-20 15:34:38 +05:30
Muhsin Keloth 6b86b6f87d chore: refactor link issue related components 2024-05-20 10:55:00 +05:30
Muhsin Keloth b5d655171e chore: refactor issue component 2024-05-20 10:31:37 +05:30
Muhsin Keloth 4263ea7db1 chore: more cleanups 2024-05-20 10:07:55 +05:30
Muhsin Keloth 3552eede47 chore: more cleanups 2024-05-20 09:57:28 +05:30
Muhsin Keloth 24825a070a feat: change search component 2024-05-18 22:46:32 +05:30
Muhsin Keloth 7767c83206 Update IssueItem.vue 2024-05-18 12:34:30 +05:30
Muhsin Keloth 64324d239e chore: style changes 2024-05-18 12:08:22 +05:30
Muhsin KelothandGitHub bd55bb1f08 Merge branch 'feat/linear-backend' into feature/cw-3271 2024-05-17 20:26:28 +05:30
Muhsin Keloth ca8dc3760f chore: update linear config 2024-05-17 20:26:09 +05:30
Muhsin Keloth 6471db4ba7 chore: code cleanup 2024-05-17 20:23:55 +05:30
Muhsin Keloth d7e7e4d8c9 Update IssueItem.vue 2024-05-17 16:49:22 +05:30
Muhsin KelothandGitHub d169d97987 Merge branch 'develop' into feat/linear-backend 2024-05-17 16:45:03 +05:30
Muhsin Keloth 480888e115 chore: unused image 2024-05-17 16:20:03 +05:30
Muhsin Keloth 1fb84c6402 chore: change everything to vue3 syntax 2024-05-17 16:09:08 +05:30
Muhsin Keloth 2c2e000264 Merge branch 'feature/cw-3271' of https://github.com/chatwoot/chatwoot into feature/cw-3271 2024-05-17 16:08:43 +05:30
Muhsin Keloth 3376183b03 chore: migrate linear base to vue3 syntax 2024-05-17 15:14:38 +05:30
Muhsin KelothandGitHub 8159aad772 Merge branch 'feat/linear-backend' into feature/cw-3271 2024-05-17 15:01:26 +05:30
Muhsin Keloth c26e0cc7e0 Merge branch 'feat/linear-backend' of https://github.com/chatwoot/chatwoot into feat/linear-backend 2024-05-17 15:01:09 +05:30
Muhsin Keloth 2378efd853 Update linear_queries.rb 2024-05-17 15:00:28 +05:30
Muhsin KelothandGitHub b40c3110aa Merge branch 'develop' into feat/linear-backend 2024-05-17 15:00:04 +05:30
Muhsin Keloth 3d1bafa3c0 feat: complete basic front end 2024-05-17 14:04:48 +05:30
Muhsin Keloth 08c487d1c3 chore: update issue query 2024-05-17 12:08:32 +05:30
Muhsin Keloth 0e11dac471 feat: create basic front end 2024-05-17 12:06:19 +05:30
Muhsin KelothandGitHub e992c23d03 Merge branch 'feat/linear-backend' into feat/linear-frontend 2024-05-16 17:30:58 +05:30
Muhsin Keloth 669a138114 chore: team validation 2024-05-16 16:20:24 +05:30
Muhsin Keloth 31732a1a7b chore: replace graphlient with HTTParty 2024-05-16 16:18:47 +05:30
Muhsin KelothandGitHub b663f3201c Merge branch 'develop' into feat/linear-backend 2024-05-16 15:25:33 +05:30
Muhsin Keloth 01ddb61c9e chore: add url in issue 2024-05-16 15:25:10 +05:30
Muhsin Keloth 87e8e342c9 Update index.vue 2024-05-14 12:27:52 +05:30
Muhsin Keloth 27ec845faa chore: add url in issue 2024-05-14 08:28:24 +05:30
iamsivin 91cefe2072 chore: Fix input styles 2024-05-14 00:55:54 +05:30
Muhsin KelothandGitHub d356067300 Merge branch 'develop' into feat/linear-backend 2024-05-13 22:06:57 +05:30
Muhsin Keloth aa3f2b56bc chore: minor fix 2024-05-13 21:08:59 +05:30
Muhsin Keloth bc5449f208 chore: translation updates 2024-05-13 19:13:03 +05:30
Muhsin Keloth e1bfd9a17a chore: translation updates 2024-05-13 19:04:06 +05:30
Muhsin Keloth 4825ab1a29 chore: code cleanup 2024-05-13 18:34:40 +05:30
iamsivin 7efc2b9325 chore: Style fix 2024-05-13 14:33:01 +05:30
Muhsin Keloth 4a491d8a91 feat: add or link issue 2024-05-13 13:59:38 +05:30
Muhsin Keloth f2db469c75 feat: add basic create issue feature 2024-05-10 16:59:41 +05:30
Muhsin Keloth 5d48c2b1e5 chore: add more fields in get issue querey 2024-05-10 16:53:01 +05:30
Muhsin Keloth ecce6e021b chore: update link issue API 2024-05-10 15:50:13 +05:30
Muhsin Keloth 4d47fc1c25 chore: rename from team_entites to team_entities 2024-05-10 15:16:56 +05:30
Muhsin KelothandGitHub b25e395bf7 Merge branch 'develop' into feat/linear-backend 2024-05-10 11:40:56 +05:30
Muhsin KelothandGitHub 12b3323364 Merge branch 'develop' into feat/linear-backend 2024-05-10 09:44:59 +05:30
Muhsin KelothandGitHub 793420c341 Merge branch 'develop' into feat/linear-backend 2024-05-09 23:41:57 +05:30
Muhsin KelothandGitHub 775e8251a6 Merge branch 'develop' into feat/linear-backend 2024-05-09 22:45:43 +05:30
Muhsin Keloth 4645e57e9a feat: add linked issue api 2024-05-09 22:40:42 +05:30
Muhsin Keloth 5e1dbfb64d feat: search issue API 2024-05-09 22:28:47 +05:30
Muhsin Keloth df29b43540 feat: unlink issue api 2024-05-09 21:44:42 +05:30
Muhsin Keloth 3e4f43d7da chore: add specs for create and link apis 2024-05-09 16:24:31 +05:30
Muhsin Keloth 2b7c972cbf feat: add specs for linear processor 2024-05-09 15:16:14 +05:30
Muhsin Keloth 429cfd0166 feat: add specs for create and link issue 2024-05-09 15:12:20 +05:30
Muhsin Keloth fc41669178 feat: add link issue API 2024-05-09 15:05:36 +05:30
Muhsin Keloth e567be8fb2 chore: issue input validation 2024-05-09 14:25:00 +05:30
Muhsin Keloth 1ddc05dadb feat: add create issue API 2024-05-09 12:23:14 +05:30
Muhsin KelothandGitHub 0268d544d1 Merge branch 'develop' into feat/linear-backend 2024-05-09 11:33:07 +05:30
Muhsin KelothandGitHub 2960bcdf7d Merge branch 'develop' into feat/linear-backend 2024-05-09 10:37:54 +05:30
Muhsin KelothandGitHub 3c4a7bb9f7 Merge branch 'develop' into feat/linear-backend 2024-05-09 07:32:51 +05:30
Muhsin Keloth 792bc5cf9d Merge branch 'feat/linear-backend' of https://github.com/chatwoot/chatwoot into feat/linear-backend 2024-05-08 13:46:43 +05:30
Muhsin Keloth bf40242497 Merge branch 'feat/linear-backend' of https://github.com/chatwoot/chatwoot into feat/linear-backend 2024-05-08 13:46:35 +05:30
Muhsin Keloth 2130a2ebe1 Merge branch 'feat/linear-backend' of https://github.com/chatwoot/chatwoot into feat/linear-backend 2024-05-08 12:36:54 +05:30
Muhsin KelothandGitHub 2bad7f435a Merge branch 'develop' into feat/linear-backend 2024-05-08 12:36:43 +05:30
Muhsin Keloth c0d985a62b chore: make applications component in vue 3 syntax 2024-05-08 12:36:33 +05:30
Muhsin Keloth 5b7af73abe Merge branch 'feat/linear-backend' of https://github.com/chatwoot/chatwoot into feat/linear-backend 2024-05-08 12:15:44 +05:30
Muhsin Keloth 2ae79bd9a1 chore: add specs 2024-05-08 12:15:38 +05:30
Muhsin Keloth 17b61ad2b4 chore: add specs 2024-05-08 10:51:10 +05:30
Muhsin KelothandGitHub e098e463ad Merge branch 'develop' into feat/linear-backend 2024-05-08 09:09:13 +05:30
Muhsin Keloth b11f4d5bd5 Update linear_controller.rb 2024-05-07 15:22:59 +05:30
Muhsin Keloth 0a45b7e156 chore: add feature flag linear_integration 2024-05-07 15:13:33 +05:30
Muhsin Keloth ee184d9792 chore: cleanup code 2024-05-07 14:19:34 +05:30
Muhsin KelothandGitHub 3595903fb5 Merge branch 'develop' into feat/linear-backend 2024-05-07 14:06:03 +05:30
Muhsin KelothandGitHub 68a7fce45c Merge branch 'develop' into feat/linear-backend 2024-05-06 22:35:00 +05:30
Muhsin KelothandGitHub 8e5cabd14d Merge branch 'develop' into feat/linear-backend 2024-05-06 16:31:59 +05:30
Muhsin Keloth dd2938dc03 Merge branch 'feat/linear-backend' of https://github.com/chatwoot/chatwoot into feat/linear-backend 2024-05-06 15:58:56 +05:30
Muhsin Keloth 0bf4fc9e21 chore: add ProcessorService spec 2024-05-06 15:58:53 +05:30
Muhsin KelothandGitHub 8f7162d6c1 Merge branch 'develop' into feat/linear-backend 2024-05-06 14:27:21 +05:30
Muhsin Keloth 243834c401 feat: add team entities API 2024-05-06 14:26:52 +05:30
Muhsin KelothandGitHub 1bba61a758 Merge branch 'develop' into feat/linear-backend 2024-05-03 14:29:47 +05:30
Muhsin Keloth e58f1ad98d fix: specs 2024-05-03 14:25:10 +05:30
Shivam Mishra 7e61397186 fix: linear spec 2024-05-03 13:36:24 +05:30
Muhsin Keloth c5e34669f6 chore: add linear schema 2024-05-03 12:59:34 +05:30
Muhsin Keloth 97f96db60e Create linear_spec.rb 2024-05-03 12:48:14 +05:30
Muhsin Keloth 0fafe37646 chore: code cleanup 2024-05-02 16:45:57 +05:30
Muhsin Keloth 68cb6b19b0 feat: add teams end point 2024-05-02 16:11:21 +05:30
Muhsin Keloth ead7cc0c36 feat: add liner integration 2024-05-02 13:39:57 +05:30
17 changed files with 927 additions and 7 deletions
@@ -88,6 +88,6 @@ class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Bas
end
def permitted_params
params.permit(:team_id, :conversation_id, :issue_id, :link_id, :title, :description, :assignee_id, :priority, label_ids: [])
params.permit(:team_id, :project_id, :conversation_id, :issue_id, :link_id, :title, :description, :assignee_id, :priority, label_ids: [])
end
end
@@ -0,0 +1,47 @@
/* global axios */
import ApiClient from '../ApiClient';
class LinearAPI extends ApiClient {
constructor() {
super('integrations/linear', { accountScoped: true });
}
getTeams() {
return axios.get(`${this.url}/teams`);
}
getTeamEntities(teamId) {
return axios.get(`${this.url}/team_entities?team_id=${teamId}`);
}
createIssue(data) {
return axios.post(`${this.url}/create_issue`, data);
}
link_issue(conversationId, issueId, title) {
return axios.post(`${this.url}/link_issue`, {
issue_id: issueId,
conversation_id: conversationId,
title: title,
});
}
getLinkedIssue(conversationId) {
return axios.get(
`${this.url}/linked_issues?conversation_id=${conversationId}`
);
}
unlinkIssue(linkId) {
return axios.post(`${this.url}/unlink_issue`, {
link_id: linkId,
});
}
searchIssues(query) {
return axios.get(`${this.url}/search_issue?q=${query}`);
}
}
export default new LinearAPI();
@@ -1,5 +1,5 @@
<template>
<div class="resolve-actions relative flex items-center justify-end">
<div class="relative flex items-center justify-end resolve-actions">
<div class="button-group">
<woot-button
v-if="isOpen"
@@ -19,7 +19,7 @@ const props = defineProps({
default: '',
},
activeFilterId: {
type: Number,
type: [String, Number],
default: null,
},
showClearFilter: {
@@ -28,10 +28,13 @@ const props = defineProps({
},
});
const emits = defineEmits(['on-search']);
const searchTerm = ref('');
const onSearch = value => {
searchTerm.value = value;
emits('on-search', value);
};
const filteredListItems = computed(() => {
@@ -55,7 +58,7 @@ const isFilterActive = id => {
>
<slot name="search">
<dropdown-search
v-if="enableSearch && listItems.length"
v-if="enableSearch"
:input-value="searchTerm"
:input-placeholder="inputPlaceholder"
:show-clear-filter="showClearFilter"
@@ -71,6 +71,10 @@
:class="{ 'justify-end': isContactPanelOpen }"
>
<SLA-card-label v-if="hasSlaPolicyId" :chat="chat" show-extended-info />
<linear
v-if="isLinearIntegrationEnabled && isLinearFeatureEnabled"
: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,9 @@ export default {
...mapGetters({
uiFlags: 'inboxAssignableAgents/getUIFlags',
currentChat: 'getSelectedChat',
accountId: 'getCurrentAccountId',
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
appIntegrations: 'integrations/getAppIntegrations',
}),
chatMetadata() {
return this.chat.meta;
@@ -178,6 +188,17 @@ export default {
hasSlaPolicyId() {
return this.chat?.sla_policy_id;
},
isLinearIntegrationEnabled() {
return this.appIntegrations.find(
integration => integration.id === 'linear' && !!integration.hooks.length
);
},
isLinearFeatureEnabled() {
return this.isFeatureEnabledonAccount(
this.accountId,
FEATURE_FLAGS.LINEAR
);
},
},
methods: {
@@ -0,0 +1,236 @@
<template>
<div @submit.prevent="onSubmit">
<woot-input
v-model="formState.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="nameError"
@input="v$.title.$touch"
/>
<label>
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.DESCRIPTION.LABEL') }}
<textarea
v-model="formState.description"
:style="{ ...inputStyles, padding: '8px 12px' }"
rows="3"
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="formState.teamId"
:style="inputStyles"
@change="onChangeTeam"
>
<option v-for="item in teams" :key="item.name" :value="item.id">
{{ item.name }}
</option>
</select>
<span v-if="v$.teamId.$error" class="message">
{{ teamError }}
</span>
</label>
<label>
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.ASSIGNEE.LABEL') }}
<select v-model="formState.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="formState.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="formState.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="formState.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="formState.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 setup>
import { reactive, computed, onMounted, ref } from 'vue';
import { useVuelidate } from '@vuelidate/core';
import { useI18n } from 'dashboard/composables/useI18n';
import { useAlert } from 'dashboard/composables';
import LinearAPI from 'dashboard/api/integrations/linear';
import validations from './validations';
const props = defineProps({
accountId: {
type: [Number, String],
required: true,
},
conversationId: {
type: [Number, String],
required: true,
},
title: {
type: String,
default: null,
},
});
const emit = defineEmits(['close']);
const { t } = useI18n();
const teams = ref([]);
const assignees = ref([]);
const projects = ref([]);
const labels = ref([]);
const statuses = ref([]);
const priorities = [
{ id: 0, name: 'No priority' },
{ id: 1, name: 'Urgent' },
{ id: 2, name: 'High' },
{ id: 3, name: 'Normal' },
{ id: 4, name: 'Low' },
];
const isCreating = ref(false);
const inputStyles = { borderRadius: '12px', fontSize: '14px' };
const formState = reactive({
title: '',
description: '',
teamId: '',
assigneeId: '',
labelId: '',
stateId: '',
priority: '',
projectId: '',
});
const v$ = useVuelidate(validations, formState);
const isSubmitDisabled = computed(
() => v$.value.title.$invalid || isCreating.value
);
const nameError = computed(() =>
v$.value.title.$error
? t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.TITLE.REQUIRED_ERROR')
: ''
);
const teamError = computed(() =>
v$.value.teamId.$error
? t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.TEAM.REQUIRED_ERROR')
: ''
);
const onClose = () => emit('close');
const getTeams = async () => {
try {
const response = await LinearAPI.getTeams();
teams.value = response.data;
} catch (error) {
useAlert(t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.LOADING_TEAM_ERROR'));
}
};
const getTeamEntities = async () => {
try {
const response = await LinearAPI.getTeamEntities(formState.teamId);
assignees.value = response.data.users;
labels.value = response.data.labels;
statuses.value = response.data.states;
projects.value = response.data.projects;
} catch (error) {
useAlert(
t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.LOADING_TEAM_ENTITIES_ERROR')
);
}
};
const onChangeTeam = event => {
formState.teamId = event.target.value;
formState.assigneeId = '';
formState.stateId = '';
formState.labelId = '';
getTeamEntities();
};
const createIssue = async () => {
v$.value.$touch();
if (v$.value.$invalid) return;
const payload = {
team_id: formState.teamId,
title: formState.title,
description: formState.description || undefined,
assignee_id: formState.assigneeId || undefined,
project_id: formState.projectId || undefined,
state_id: formState.stateId || undefined,
priority: formState.priority || undefined,
label_ids: formState.labelId ? [formState.labelId] : undefined,
};
try {
isCreating.value = true;
const response = await LinearAPI.createIssue(payload);
const { id: issueId } = response.data;
await LinearAPI.link_issue(props.conversationId, issueId, props.title);
useAlert(t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CREATE_SUCCESS'));
onClose();
} catch (error) {
useAlert(t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CREATE_ERROR'));
} finally {
isCreating.value = false;
}
};
onMounted(getTeams);
</script>
@@ -0,0 +1,99 @@
<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="conversation.id"
:title="title"
@close="onClose"
/>
</div>
<div v-else class="flex flex-col px-8 pb-4">
<link-issue
:conversation-id="conversation.id"
:title="title"
@close="onClose"
/>
</div>
</div>
</div>
</template>
<script setup>
import { fromUnixTime, formatDistanceToNow } from 'date-fns';
import { useI18n } from 'dashboard/composables/useI18n';
import { computed, ref } from 'vue';
import LinkIssue from './LinkIssue.vue';
import CreateIssue from './CreateIssue.vue';
const props = defineProps({
accountId: {
type: [Number, String],
required: true,
},
conversation: {
type: Object,
required: true,
},
});
const { t } = useI18n();
const selectedTabIndex = ref(0);
const timeAgo = time => {
const unixTime = fromUnixTime(time);
return formatDistanceToNow(unixTime, { addSuffix: true });
};
const title = computed(() => {
const { meta: { sender: { name = null } = {} } = {}, created_at: createdAt } =
props.conversation;
return t('INTEGRATION_SETTINGS.LINEAR.LINK.LINK_TITLE', {
conversationId: props.conversation.id,
name,
timeAgo: timeAgo(createdAt),
});
});
const emits = defineEmits(['close']);
const tabs = ref([
{
key: 0,
name: t('INTEGRATION_SETTINGS.LINEAR.CREATE'),
},
{
key: 1,
name: t('INTEGRATION_SETTINGS.LINEAR.LINK.TITLE'),
},
]);
const onClose = () => {
emits('close');
};
const onClickTabChange = index => {
selectedTabIndex.value = index;
};
</script>
@@ -0,0 +1,123 @@
<script setup>
import { format } from 'date-fns';
import UserAvatarWithName from 'dashboard/components/widgets/UserAvatarWithName.vue';
import IssueHeader from './IssueHeader.vue';
import { computed } from 'vue';
const priorityMap = {
1: 'Urgent',
2: 'High',
3: 'Medium',
4: 'Low',
};
const props = defineProps({
issue: {
type: Object,
required: true,
},
linkId: {
type: String,
required: true,
},
});
const emit = defineEmits(['unlink-issue']);
const formattedDate = computed(() => {
const { createdAt } = props.issue;
return format(new Date(createdAt), 'hh:mm a, MMM dd');
});
const assignee = computed(() => {
const assigneeDetails = props.issue.assignee;
if (!assigneeDetails) return null;
const { name, avatarUrl } = assigneeDetails;
return {
name,
thumbnail: avatarUrl,
};
});
const labels = computed(() => {
return props.issue.labels?.nodes || [];
});
const priorityLabel = computed(() => {
return priorityMap[props.issue.priority];
});
const unlinkIssue = () => {
emit('unlink-issue', props.linkId);
};
</script>
<template>
<div
class="absolute flex flex-col items-start bg-ash-50 dark:bg-slate-800 z-50 px-4 py-3 border border-solid border-ash-200 w-[384px] rounded-xl gap-4 max-h-96 overflow-auto"
>
<div class="flex flex-col w-full">
<issue-header
:identifier="issue.identifier"
:link-id="linkId"
:issue-url="issue.url"
@unlink-issue="unlinkIssue"
/>
<span class="mt-2 text-sm font-medium text-ash-900">
{{ issue.title }}
</span>
<span
v-if="issue.description"
class="mt-1 text-sm text-ash-800 line-clamp-3"
>
{{ issue.description }}
</span>
</div>
<div class="flex flex-row items-center h-6 gap-2">
<user-avatar-with-name v-if="assignee" :user="assignee" class="py-1" />
<div v-if="assignee" class="w-px h-3 bg-ash-200" />
<div class="flex items-center gap-1 py-1">
<fluent-icon
icon="status"
size="14"
:style="{ color: issue.state.color }"
/>
<h6 class="text-xs text-ash-900">
{{ issue.state.name }}
</h6>
</div>
<div v-if="priorityLabel" class="w-px h-3 bg-ash-200" />
<div v-if="priorityLabel" class="flex items-center gap-1 py-1">
<fluent-icon
:icon="`priority-${priorityLabel.toLowerCase()}`"
size="14"
view-box="0 0 12 12"
/>
<h6 class="text-xs text-ash-900">{{ priorityLabel }}</h6>
</div>
</div>
<div v-if="labels.length" class="flex flex-wrap 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">
{{
$t('INTEGRATION_SETTINGS.LINEAR.ISSUE.CREATED_AT', {
createdAt: formattedDate,
})
}}
</span>
</div>
</div>
</template>
@@ -0,0 +1,56 @@
<template>
<div class="flex flex-row justify-between">
<div
class="flex items-center justify-center gap-1 h-[24px] px-2 py-1 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">{{ identifier }}</span>
</div>
<div class="flex items-center gap-0.5">
<woot-button
variant="clear"
color-scheme="secondary"
class="h-[24px]"
@click="unlinkIssue"
>
<fluent-icon icon="unlink" size="12" type="outline" icon-lib="lucide" />
</woot-button>
<woot-button
variant="clear"
class="h-[24px]"
color-scheme="secondary"
@click="openIssue"
>
<fluent-icon icon="arrow-up-right" size="14" />
</woot-button>
</div>
</div>
</template>
<script setup>
const props = defineProps({
identifier: {
type: String,
required: true,
},
issueUrl: {
type: String,
required: true,
},
});
const emit = defineEmits(['unlink-issue']);
const unlinkIssue = () => {
emit('unlink-issue');
};
const openIssue = () => {
window.open(props.issueUrl, '_blank');
};
</script>
@@ -0,0 +1,131 @@
<template>
<div
class="flex flex-col justify-between"
:class="shouldShowDropdown ? 'h-[256px]' : 'gap-2'"
>
<filter-button
right-icon="chevron-down"
:button-text="linkIssueTitle"
class="justify-between w-full bg-slate-50 dark:bg-slate-800 hover:bg-slate-75 dark:hover:bg-slate-800"
@click="toggleDropdown"
>
<template v-if="shouldShowDropdown" #dropdown>
<filter-list-dropdown
v-if="issues"
v-on-clickaway="toggleDropdown"
:show-clear-filter="false"
:list-items="issues"
:active-filter-id="selectedOption.id"
:input-placeholder="$t('INTEGRATION_SETTINGS.LINEAR.LINK.SEARCH')"
enable-search
class="left-0 flex flex-col w-full overflow-y-auto h-fit max-h-[160px] md:left-auto md:right-0 top-10"
@on-search="onSearch"
@click="onSelectIssue"
/>
</template>
</filter-button>
<div class="flex items-center justify-end w-full gap-2">
<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 setup>
import { ref, computed } from 'vue';
import { useI18n } from 'dashboard/composables/useI18n';
import { useAlert } from 'dashboard/composables';
import LinearAPI from 'dashboard/api/integrations/linear';
import FilterButton from 'dashboard/components/ui/Dropdown/DropdownButton.vue';
import FilterListDropdown from 'dashboard/components/ui/Dropdown/DropdownList.vue';
const props = defineProps({
conversationId: {
type: [Number, String],
required: true,
},
title: {
type: String,
default: null,
},
});
const emits = defineEmits(['close']);
const { t } = useI18n();
const issues = ref([]);
const shouldShowDropdown = ref(false);
const selectedOption = ref({ id: null, name: '' });
const isFetching = ref(false);
const isLinking = ref(false);
const searchQuery = ref('');
const toggleDropdown = () => {
issues.value = [];
shouldShowDropdown.value = !shouldShowDropdown.value;
};
const linkIssueTitle = computed(() => {
return selectedOption.value.id
? selectedOption.value.name
: t('INTEGRATION_SETTINGS.LINEAR.LINK.SELECT');
});
const isSubmitDisabled = computed(() => {
return !selectedOption.value.id || isLinking.value;
});
const onSelectIssue = item => {
selectedOption.value = item;
toggleDropdown();
};
const onClose = () => {
emits('close');
};
const onSearch = async value => {
if (!value) return;
searchQuery.value = value;
try {
isFetching.value = true;
const response = await LinearAPI.searchIssues(value);
issues.value = response.data.map(issue => ({
id: issue.id,
name: `${issue.identifier} ${issue.title}`,
}));
} catch (error) {
useAlert(t('INTEGRATION_SETTINGS.LINEAR.LINK.ERROR'));
} finally {
isFetching.value = false;
}
};
const linkIssue = async () => {
const { id: issueId } = selectedOption.value;
try {
isLinking.value = true;
await LinearAPI.link_issue(props.conversationId, issueId, props.title);
useAlert(t('INTEGRATION_SETTINGS.LINEAR.LINK.LINK_SUCCESS'));
searchQuery.value = '';
issues.value = [];
onClose();
} catch (error) {
useAlert(t('INTEGRATION_SETTINGS.LINEAR.LINK.LINK_ERROR'));
} finally {
isLinking.value = false;
}
};
</script>
@@ -0,0 +1,121 @@
<template>
<div class="relative" :class="{ group: linkedIssue }">
<woot-button
v-on-clickaway="closeIssue"
v-tooltip="tooltipText"
variant="clear"
color-scheme="secondary"
@click="openIssue"
>
<fluent-icon
icon="linear"
size="19"
class="text-[#5E6AD2]"
view-box="0 0 19 19"
/>
<span v-if="linkedIssue" class="text-xs font-medium text-ash-800">
{{ linkedIssue.issue.identifier }}
</span>
</woot-button>
<issue
v-if="linkedIssue"
:issue="linkedIssue.issue"
:link-id="linkedIssue.id"
class="absolute right-0 top-[40px] invisible group-hover:visible"
@unlink-issue="unlinkIssue"
/>
<woot-modal
:show.sync="shouldShowPopup"
:on-close="closePopup"
class="!items-start [&>div]:!top-12"
>
<create-or-link-issue
:conversation="conversation"
:account-id="currentAccountId"
@close="closePopup"
/>
</woot-modal>
</div>
</template>
<script setup>
import { computed, ref, onMounted, watch } from 'vue';
import { useAlert } from 'dashboard/composables';
import { useStoreGetters } from 'dashboard/composables/store';
import { useI18n } from 'dashboard/composables/useI18n';
import LinearAPI from 'dashboard/api/integrations/linear';
import CreateOrLinkIssue from './CreateOrLinkIssue.vue';
import Issue from './Issue.vue';
const props = defineProps({
conversationId: {
type: [Number, String],
required: true,
},
});
const getters = useStoreGetters();
const { t } = useI18n();
const linkedIssue = ref(null);
const shouldShow = ref(false);
const shouldShowPopup = ref(false);
const currentAccountId = getters.getCurrentAccountId;
const conversation = computed(() =>
getters.getConversationById.value(props.conversationId)
);
const tooltipText = computed(() => {
return linkedIssue.value === null
? t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK_BUTTON')
: null;
});
const loadLinkedIssue = async () => {
linkedIssue.value = null;
try {
const response = await LinearAPI.getLinkedIssue(props.conversationId);
const issues = response.data;
linkedIssue.value = issues && issues.length ? issues[0] : null;
} catch (error) {
useAlert(error?.message || t('INTEGRATION_SETTINGS.LINEAR.LOADING_ERROR'));
}
};
const unlinkIssue = async linkId => {
try {
await LinearAPI.unlinkIssue(linkId);
linkedIssue.value = null;
useAlert(t('INTEGRATION_SETTINGS.LINEAR.UNLINK.SUCCESS'));
} catch (error) {
useAlert(t('INTEGRATION_SETTINGS.LINEAR.UNLINK.ERROR'));
}
};
const openIssue = () => {
if (!linkedIssue.value) shouldShowPopup.value = true;
shouldShow.value = true;
};
const closePopup = () => {
shouldShowPopup.value = false;
loadLinkedIssue();
};
const closeIssue = () => {
shouldShow.value = false;
};
watch(
() => props.conversationId,
() => {
loadLinkedIssue();
}
);
onMounted(() => {
loadLinkedIssue();
});
</script>
@@ -0,0 +1,10 @@
import { required } from '@vuelidate/validators';
export default {
title: {
required,
},
teamId: {
required,
},
};
+1
View File
@@ -30,4 +30,5 @@ export const FEATURE_FLAGS = {
EMAIL_CONTINUITY_ON_API_CHANNEL: 'email_continuity_on_api_channel',
INBOUND_EMAILS: 'inbound_emails',
IP_LOOKUP: 'ip_lookup',
LINEAR: 'linear_integration',
};
@@ -203,6 +203,75 @@
"API_SUCCESS": "Dashboard app deleted successfully",
"API_ERROR": "We couldn't delete the app. Please try again later"
}
},
"LINEAR": {
"ADD_OR_LINK_BUTTON": "Create/Link Linear Issue",
"LOADING": "Fetching linear issues...",
"LOADING_ERROR": "There was an error fetching the linear issues, please try again",
"CREATE": "Create",
"LINK": {
"SEARCH": "Search issues",
"SELECT": "Select issue",
"TITLE": "Link",
"EMPTY_LIST": "No linear issues found",
"LOADING": "Loading",
"ERROR": "There was an error fetching the linear issues, please try again",
"LINK_SUCCESS": "Issue linked successfully",
"LINK_ERROR": "There was an error linking the issue, please try again",
"LINK_TITLE": "#%{conversationId}: %{name} - Created %{timeAgo}"
},
"ADD_OR_LINK": {
"TITLE": "Create/link linear issue",
"DESCRIPTION": "Create Linear issues from conversations, or link existing ones for seamless tracking.",
"FORM": {
"TITLE": {
"LABEL": "Title",
"PLACEHOLDER": "Enter title",
"REQUIRED_ERROR": "Title is required"
},
"DESCRIPTION": {
"LABEL": "Description",
"PLACEHOLDER": "Enter description"
},
"TEAM": {
"LABEL": "Team",
"REQUIRED_ERROR": "Team is required"
},
"ASSIGNEE": {
"LABEL": "Assignee"
},
"PRIORITY": {
"LABEL": "Priority"
},
"LABEL": {
"LABEL": "Label"
},
"STATUS": {
"LABEL": "Status"
},
"PROJECT": {
"LABEL": "Project"
}
},
"CREATE": "Create",
"CANCEL": "Cancel",
"CREATE_SUCCESS": "Issue created successfully",
"CREATE_ERROR": "There was an error creating the issue, please try again",
"LOADING_TEAM_ERROR": "There was an error fetching the teams, please try again",
"LOADING_TEAM_ENTITIES_ERROR": "There was an error fetching the team entities, please try again"
},
"ISSUE": {
"STATUS": "Status",
"PRIORITY": "Priority",
"ASSIGNEE": "Assignee",
"LABELS": "Labels",
"CREATED_AT": "Created at %{createdAt}"
},
"UNLINK": {
"TITLE": "Unlink",
"SUCCESS": "Issue unlinked successfully",
"ERROR": "There was an error unlinking the issue, please try again"
}
}
}
}
@@ -145,7 +145,6 @@ import CustomAttributes from './customAttributes/CustomAttributes.vue';
import draggable from 'vuedraggable';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import MacrosList from './Macros/List.vue';
export default {
components: {
AccordionItem,
@@ -275,5 +275,8 @@
"chevrons-right-outline": ["m6 17 5-5-5-5", "m13 17 5-5-5-5"],
"chevron-right-single-outline": "m9 18 6-6-6-6",
"avatar-upload-outline": "M19.754 11a.75.75 0 0 1 .743.648l.007.102v7a3.25 3.25 0 0 1-3.065 3.246l-.185.005h-11a3.25 3.25 0 0 1-3.244-3.066l-.006-.184V11.75a.75.75 0 0 1 1.494-.102l.006.102v7a1.75 1.75 0 0 0 1.607 1.745l.143.006h11A1.75 1.75 0 0 0 19 18.894l.005-.143V11.75a.75.75 0 0 1 .75-.75ZM6.22 7.216l4.996-4.996a.75.75 0 0 1 .976-.073l.084.072l5.005 4.997a.75.75 0 0 1-.976 1.134l-.084-.073l-3.723-3.716l.001 11.694a.75.75 0 0 1-.648.743l-.102.007a.75.75 0 0 1-.743-.648L11 16.255V4.558L7.28 8.277a.75.75 0 0 1-.976.073l-.084-.073a.75.75 0 0 1-.073-.977l.073-.084l4.996-4.996L6.22 7.216Z",
"text-copy-outline": "M5.503 4.627L5.5 6.75v10.504a3.25 3.25 0 0 0 3.25 3.25h8.616a2.25 2.25 0 0 1-2.122 1.5H8.75A4.75 4.75 0 0 1 4 17.254V6.75c0-.98.627-1.815 1.503-2.123M17.75 2A2.25 2.25 0 0 1 20 4.25v13a2.25 2.25 0 0 1-2.25 2.25h-9a2.25 2.25 0 0 1-2.25-2.25v-13A2.25 2.25 0 0 1 8.75 2zm0 1.5h-9a.75.75 0 0 0-.75.75v13c0 .414.336.75.75.75h9a.75.75 0 0 0 .75-.75v-13a.75.75 0 0 0-.75-.75"
"text-copy-outline": "M5.503 4.627L5.5 6.75v10.504a3.25 3.25 0 0 0 3.25 3.25h8.616a2.25 2.25 0 0 1-2.122 1.5H8.75A4.75 4.75 0 0 1 4 17.254V6.75c0-.98.627-1.815 1.503-2.123M17.75 2A2.25 2.25 0 0 1 20 4.25v13a2.25 2.25 0 0 1-2.25 2.25h-9a2.25 2.25 0 0 1-2.25-2.25v-13A2.25 2.25 0 0 1 8.75 2zm0 1.5h-9a.75.75 0 0 0-.75.75v13c0 .414.336.75.75.75h9a.75.75 0 0 0 .75-.75v-13a.75.75 0 0 0-.75-.75",
"linear-outline": "M1.17156 10.4618C1.14041 10.329 1.2986 10.2454 1.39505 10.3418L6.50679 15.4536C6.60323 15.55 6.5196 15.7082 6.38681 15.6771C3.80721 15.0719 1.77669 13.0414 1.17156 10.4618ZM1.00026 8.4131C0.997795 8.45277 1.01271 8.49149 1.0408 8.51959L8.32904 15.8078C8.35714 15.8359 8.39586 15.8509 8.43553 15.8484C8.76721 15.8277 9.09266 15.784 9.41026 15.7187C9.51729 15.6968 9.55447 15.5653 9.47721 15.488L1.36063 7.37142C1.28337 7.29416 1.15187 7.33134 1.12989 7.43837C1.06466 7.75597 1.02092 8.08142 1.00026 8.4131ZM1.58953 6.00739C1.56622 6.05972 1.57809 6.12087 1.6186 6.16139L10.6872 15.23C10.7278 15.2705 10.7889 15.2824 10.8412 15.2591C11.0913 15.1477 11.3336 15.0221 11.5672 14.8833C11.6445 14.8374 11.6564 14.7312 11.5929 14.6676L2.18099 5.25577C2.11742 5.1922 2.01121 5.20412 1.96529 5.28142C1.8265 5.51499 1.70091 5.75733 1.58953 6.00739ZM2.77222 4.37899C2.7204 4.32718 2.7172 4.24407 2.76602 4.18942C4.04913 2.75294 5.9156 1.84863 7.99327 1.84863C11.863 1.84863 15 4.98565 15 8.85536C15 10.933 14.0957 12.7995 12.6592 14.0826C12.6046 14.1314 12.5215 14.1282 12.4696 14.0764L2.77222 4.37899Z",
"status-outline": "m8.462 6.81l3.284 13.616c.178.737 1.211.775 1.443.054l3.257-10.122l.586 2.095a.75.75 0 0 0 .722.548h3.494a.75.75 0 0 0 0-1.5h-2.925l-1.105-3.95c-.2-.717-1.208-.736-1.436-.028l-3.203 9.957L9.224 3.574c-.182-.757-1.255-.769-1.454-.016l-2.1 7.943H2.75a.75.75 0 0 0 0 1.5h3.496a.75.75 0 0 0 .725-.558z",
"unlink-outline": "m18.84 12.25l1.72-1.71h-.02a5.004 5.004 0 0 0-.12-7.07a5.006 5.006 0 0 0-6.95 0l-1.72 1.71m-6.58 6.57l-1.71 1.71a5.004 5.004 0 0 0 .12 7.07a5.006 5.006 0 0 0 6.95 0l1.71-1.71M8 2v3M2 8h3m11 11v3m3-6h3"
}
+2 -1
View File
@@ -56,7 +56,8 @@ class Linear
description: params[:description],
assigneeId: params[:assignee_id],
priority: params[:priority],
labelIds: params[:label_ids]
labelIds: params[:label_ids],
projectId: params[:project_id]
}.compact
mutation = Linear::Mutations.issue_create(variables)
response = post({ query: mutation })