Compare commits
2
Commits
v4.9.2
...
feat/linear
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e90600c03 | ||
|
|
19410dfa3d |
@@ -23,6 +23,15 @@
|
||||
:key="element.name"
|
||||
class="bg-white dark:bg-gray-800"
|
||||
>
|
||||
<div v-if="element.name === 'linear'" 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"
|
||||
@@ -145,6 +154,7 @@ import CustomAttributes from './customAttributes/CustomAttributes.vue';
|
||||
import draggable from 'vuedraggable';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import MacrosList from './Macros/List.vue';
|
||||
import Linear from './linear/index.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -157,6 +167,7 @@ export default {
|
||||
ConversationParticipant,
|
||||
draggable,
|
||||
MacrosList,
|
||||
Linear,
|
||||
},
|
||||
mixins: [alertMixin, uiSettingsMixin],
|
||||
props: {
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-auto overflow-auto">
|
||||
<woot-modal-header
|
||||
header-title="Create/link linear issue"
|
||||
header-content="Quickly create/link a linear issue from this conversation"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col h-auto overflow-auto">
|
||||
<div class="flex flex-col px-8 pb-4" @submit.prevent="onSubmit">
|
||||
<woot-input
|
||||
v-model="title"
|
||||
label="Title"
|
||||
class="w-full"
|
||||
placeholder="Enter title"
|
||||
type="text"
|
||||
@blur="$v.title.$touch"
|
||||
/>
|
||||
<label>
|
||||
Description
|
||||
<textarea
|
||||
v-model="description"
|
||||
rows="6"
|
||||
type="text"
|
||||
placeholder="Enter description"
|
||||
@blur="$v.description.$touch"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Team
|
||||
<select v-model="teamId" @change="onChangeTeam($event)">
|
||||
<option v-for="item in teams" :key="item.name" :value="item.id">
|
||||
{{ item.name }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Assignee
|
||||
<select v-model="assigneeId">
|
||||
<option v-for="item in assignees" :key="item.id" :value="item.id">
|
||||
{{ item.name }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Priority
|
||||
<select v-model="priority">
|
||||
<option v-for="item in priorities" :key="item.id" :value="item.id">
|
||||
{{ item.name }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Label
|
||||
<select v-model="labelId">
|
||||
<option v-for="item in labels" :key="item.id" :value="item.id">
|
||||
{{ item.name }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Status
|
||||
<select v-model="stateId">
|
||||
<option v-for="item in statuses" :key="item.id" :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('SLA.FORM.CANCEL') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
:is-disabled="isSubmitDisabled"
|
||||
class="px-4 rounded-xl"
|
||||
:is-loading="isCreating"
|
||||
@click.prevent="createIssue"
|
||||
>
|
||||
Create
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { frontendURL, conversationUrl } from 'dashboard/helper/URLHelper.js';
|
||||
import { LinearClient } from '@linear/sdk';
|
||||
|
||||
// const chatwootAPIKey = '';
|
||||
const localAPIKey = '';
|
||||
const linearClient = new LinearClient({
|
||||
apiKey: localAPIKey,
|
||||
});
|
||||
// const BASE_URL = 'https:app.chatwoot.com';
|
||||
const BASE_URL = 'http://localhost:3000';
|
||||
|
||||
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: '',
|
||||
stateId: '',
|
||||
labelId: '',
|
||||
priority: 0,
|
||||
teams: [],
|
||||
assignees: [],
|
||||
labels: [],
|
||||
statuses: [],
|
||||
searchTerm: '',
|
||||
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',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
validations,
|
||||
computed: {
|
||||
isSubmitDisabled() {
|
||||
return this.$v.title.$invalid || this.isCreating;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.getTeams();
|
||||
this.getAssignees();
|
||||
this.getLabels();
|
||||
this.getWorkflowStates();
|
||||
},
|
||||
methods: {
|
||||
onClose() {
|
||||
this.$emit('close');
|
||||
},
|
||||
onChangeTeam(event) {
|
||||
this.teamId = event.target.value;
|
||||
this.getLabels();
|
||||
this.getWorkflowStates();
|
||||
this.getAssignees();
|
||||
},
|
||||
async getTeams() {
|
||||
const teams = await linearClient.teams();
|
||||
this.teams = teams.nodes;
|
||||
},
|
||||
async getAssignees() {
|
||||
const assignees = await linearClient.users();
|
||||
this.assignees = assignees.nodes;
|
||||
},
|
||||
async getLabels() {
|
||||
const labels = await linearClient.issueLabels({
|
||||
teamId: this.teamId,
|
||||
});
|
||||
this.labels = labels.nodes;
|
||||
},
|
||||
async getWorkflowStates() {
|
||||
const states = await linearClient.workflowStates();
|
||||
this.statuses = states.nodes;
|
||||
},
|
||||
async createIssue() {
|
||||
this.isCreating = true;
|
||||
const response = await linearClient.createIssue({
|
||||
teamId: this.teamId,
|
||||
title: this.title,
|
||||
description: this.description,
|
||||
assigneeId: this.assigneeId,
|
||||
priority: this.priority,
|
||||
labelIds: this.labelId ? [this.labelId] : [],
|
||||
});
|
||||
const stringifyResponse = JSON.stringify(response);
|
||||
const {
|
||||
_issue: { id: issueId },
|
||||
} = JSON.parse(stringifyResponse);
|
||||
this.attachmentLinkURL(issueId);
|
||||
this.showAlert('Linear issue created successfully');
|
||||
},
|
||||
async attachmentLinkURL(issueId) {
|
||||
const url =
|
||||
BASE_URL +
|
||||
frontendURL(
|
||||
conversationUrl({
|
||||
accountId: this.accountId,
|
||||
id: this.conversationId,
|
||||
})
|
||||
);
|
||||
await linearClient.attachmentLinkURL(issueId, url);
|
||||
this.isCreating = false;
|
||||
this.onClose();
|
||||
},
|
||||
|
||||
openSearch() {
|
||||
this.showSearchBox = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between group">
|
||||
<!-- <woot-button
|
||||
size="tiny"
|
||||
variant="link"
|
||||
color-scheme="secondary"
|
||||
icon="delete"
|
||||
@click="deleteIssue(issue)"
|
||||
/> -->
|
||||
<a
|
||||
:href="issue.link"
|
||||
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="deleteIssue(issue)"
|
||||
>
|
||||
Unlink
|
||||
</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"
|
||||
>
|
||||
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"
|
||||
>
|
||||
Todo
|
||||
</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"
|
||||
>
|
||||
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"
|
||||
>
|
||||
{{ issue.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"
|
||||
>
|
||||
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"
|
||||
>
|
||||
Muhsin
|
||||
</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"
|
||||
>
|
||||
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"
|
||||
>
|
||||
---
|
||||
</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"
|
||||
>
|
||||
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';
|
||||
export default {
|
||||
props: {
|
||||
issue: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
deleteIssue(issue) {
|
||||
this.$emit('delete', issue);
|
||||
},
|
||||
formatDate(timestamp) {
|
||||
return format(timestamp, 'MMM dd, hh:mm a');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-row gap-2">
|
||||
<woot-button
|
||||
v-if="!isLoading && !issues.length"
|
||||
variant="smooth"
|
||||
icon="add"
|
||||
size="tiny"
|
||||
@click="shoeCreateIssuePopup"
|
||||
>
|
||||
Create or Link Issue
|
||||
</woot-button>
|
||||
</div>
|
||||
<div v-if="isLoading" class="flex items-center justify-center">
|
||||
<span class="text-sm font-medium text-slate-800 dark:text-slate-100">
|
||||
Loading..
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="!isLoading && issues.length" class="flex flex-col gap-2">
|
||||
<issue-item
|
||||
v-for="issue in issues"
|
||||
:key="issue.id"
|
||||
:issue="issue"
|
||||
:conversation-id="conversationId"
|
||||
@delete="deleteIssue"
|
||||
/>
|
||||
</div>
|
||||
<woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup">
|
||||
<add-issue
|
||||
:conversation-id="conversationId"
|
||||
:account-id="currentAccountId"
|
||||
@close="hideAddPopup"
|
||||
/>
|
||||
</woot-modal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { LinearClient } from '@linear/sdk';
|
||||
|
||||
import { mapGetters } from 'vuex';
|
||||
import IssueItem from './IssueItem.vue';
|
||||
import accountMixin from 'dashboard/mixins/account.js';
|
||||
import AddIssue from './AddIssue.vue';
|
||||
|
||||
import { frontendURL, conversationUrl } from 'dashboard/helper/URLHelper.js';
|
||||
|
||||
// const chatwootAPIKey = '';
|
||||
const localAPIKey = '';
|
||||
const linearClient = new LinearClient({
|
||||
apiKey: localAPIKey,
|
||||
});
|
||||
// const BASE_URL = 'https:app.chatwoot.com';
|
||||
const BASE_URL = 'http://localhost:3000';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
IssueItem,
|
||||
AddIssue,
|
||||
},
|
||||
mixins: [accountMixin],
|
||||
props: {
|
||||
conversationId: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
showAddPopup: false,
|
||||
issues: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentAccountId: 'getCurrentAccountId',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
this.loadIssues();
|
||||
},
|
||||
methods: {
|
||||
shoeCreateIssuePopup() {
|
||||
this.showAddPopup = true;
|
||||
},
|
||||
hideAddPopup() {
|
||||
this.showAddPopup = false;
|
||||
this.loadIssues();
|
||||
},
|
||||
async loadIssues() {
|
||||
const url =
|
||||
BASE_URL +
|
||||
frontendURL(
|
||||
conversationUrl({
|
||||
accountId: this.accountId,
|
||||
id: this.conversationId,
|
||||
})
|
||||
);
|
||||
this.isLoading = true;
|
||||
linearClient.attachmentsForURL(url).then(issues => {
|
||||
if (issues.nodes.length) {
|
||||
issues.nodes.map(issue1 => {
|
||||
const linearIssueId = issue1._issue.id;
|
||||
const attachmentId = issue1.id;
|
||||
if (linearIssueId) {
|
||||
linearClient.issue(linearIssueId).then(issue2 => {
|
||||
console.log('Issue:', issue2, attachmentId);
|
||||
this.issues.push({
|
||||
id: issue2.id,
|
||||
title: issue2.title,
|
||||
link: issue2.url,
|
||||
identifier: issue2.identifier,
|
||||
attachmentId,
|
||||
...issue2,
|
||||
});
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.isLoading = false;
|
||||
console.log('No issues');
|
||||
}
|
||||
});
|
||||
},
|
||||
async deleteIssue(issue) {
|
||||
const index = this.issues.findIndex(i => i.id === issue.id);
|
||||
if (index > -1) {
|
||||
this.issues.splice(index, 1);
|
||||
}
|
||||
await linearClient.deleteAttachment(issue.attachmentId);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.macros_list--empty-state {
|
||||
padding: var(--space-slab);
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
.macros_add-button {
|
||||
margin: var(--space-small) auto 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,12 @@
|
||||
import { required } from 'vuelidate/lib/validators';
|
||||
|
||||
export default {
|
||||
title: {
|
||||
required,
|
||||
},
|
||||
description: {
|
||||
required,
|
||||
},
|
||||
teamId: {},
|
||||
assigneeId: {},
|
||||
};
|
||||
+2
-1
@@ -35,6 +35,7 @@
|
||||
"@chatwoot/utils": "^0.0.23",
|
||||
"@hcaptcha/vue-hcaptcha": "^0.3.2",
|
||||
"@june-so/analytics-next": "^2.0.0",
|
||||
"@linear/sdk": "^20.0.0",
|
||||
"@radix-ui/colors": "^1.0.1",
|
||||
"@rails/actioncable": "6.1.3",
|
||||
"@rails/ujs": "^7.0.3-1",
|
||||
@@ -167,4 +168,4 @@
|
||||
"scss-lint"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3501,6 +3501,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.50.0.tgz#9e93b850f0f3fa35f5fa59adfd03adae8488e484"
|
||||
integrity sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==
|
||||
|
||||
"@graphql-typed-document-node/core@^3.1.0":
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861"
|
||||
integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==
|
||||
|
||||
"@hcaptcha/vue-hcaptcha@^0.3.2":
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@hcaptcha/vue-hcaptcha/-/vue-hcaptcha-0.3.2.tgz#0f77d6fc19bc47eadb6b2181eee5fc132441a942"
|
||||
@@ -3872,6 +3877,15 @@
|
||||
typescript "^4.9.5"
|
||||
unfetch "^4.1.0"
|
||||
|
||||
"@linear/sdk@^20.0.0":
|
||||
version "20.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@linear/sdk/-/sdk-20.0.0.tgz#a15afb3abf3cd2d48375f42d39d99d127dc2aa17"
|
||||
integrity sha512-LljgBsSRzU++Jl7eIJhL8Z8Btso+rFN4lWeV3fg0RVfONlW0PEPLkaKJDBdA2NQ2TzpMTCJ6xcDwvIvDSQo6Ww==
|
||||
dependencies:
|
||||
"@graphql-typed-document-node/core" "^3.1.0"
|
||||
graphql "^15.4.0"
|
||||
isomorphic-unfetch "^3.1.0"
|
||||
|
||||
"@lit-labs/ssr-dom-shim@^1.0.0", "@lit-labs/ssr-dom-shim@^1.1.0":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.1.tgz#64df34e2f12e68e78ac57e571d25ec07fa460ca9"
|
||||
@@ -11934,6 +11948,11 @@ graphemer@^1.4.0:
|
||||
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
|
||||
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
|
||||
|
||||
graphql@^15.4.0:
|
||||
version "15.8.0"
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.8.0.tgz#33410e96b012fa3bdb1091cc99a94769db212b38"
|
||||
integrity sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==
|
||||
|
||||
handle-thing@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e"
|
||||
|
||||
Reference in New Issue
Block a user