Compare commits

...
2 Commits
Author SHA1 Message Date
Muhsin Keloth 6e90600c03 chore: add packages 2024-04-30 09:13:36 +05:30
Muhsin Keloth 19410dfa3d feat: add linear 2024-04-29 13:27:18 +05:30
7 changed files with 561 additions and 1 deletions
@@ -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
View File
@@ -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"
]
}
}
}
+19
View File
@@ -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"