feat: Enhance Linear integration UX with multi-issue support and improved placement (#11668)

Fixes
https://linear.app/chatwoot/issue/CW-4150/support-for-multiple-issues-linking-in-linear

This PR significantly improves the Linear integration user experience by
relocating the Linear integration from the conversation header to the
contact panel and adding support for multiple issue linking per
conversation.

  ### Key Changes

- **Relocated Linear integration**: Moved from conversation header to
contact panel for better organization and accessibility
- **Multi-issue support**: Added ability to link/create multiple Linear
issues for a single conversation
- **Integration CTA**: Added a dedicated call-to-action section for
users who haven't connected their Linear account yet
  - **UI/UX improvements**: Enhanced design consistency and user flow




<details>
<summary>Screenshots</summary>

  #### Multiple Issues Support


![link-multiple-issues](https://github.com/user-attachments/assets/b56cfa7d-6f98-42db-b4bb-361ae59d0eae)

  #### Integration CTA


![link-multiple-issues](https://github.com/user-attachments/assets/a895fcbe-780a-47f8-9fa4-3a2af8b243e1)

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: Pranav <pranavrajs@gmail.com>
This commit is contained in:
Muhsin Keloth
2025-06-11 01:10:02 +05:30
committed by GitHub
parent 4a83e70158
commit 4303007786
11 changed files with 494 additions and 465 deletions

View File

@@ -19,6 +19,9 @@ import Draggable from 'vuedraggable';
import MacrosList from './Macros/List.vue';
import ShopifyOrdersList from 'dashboard/components/widgets/conversation/ShopifyOrdersList.vue';
import SidebarActionsHeader from 'dashboard/components-next/SidebarActionsHeader.vue';
import LinearIssuesList from 'dashboard/components/widgets/conversation/linear/IssuesList.vue';
import LinearSetupCTA from 'dashboard/components/widgets/conversation/linear/LinearSetupCTA.vue';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
const props = defineProps({
conversationId: {
@@ -40,6 +43,13 @@ const {
const dragging = ref(false);
const conversationSidebarItems = ref([]);
const currentAccountId = useMapGetter('getCurrentAccountId');
const isFeatureEnabledonAccount = useMapGetter(
'accounts/isFeatureEnabledonAccount'
);
const shopifyIntegration = useFunctionGetter(
'integrations/getIntegration',
'shopify'
@@ -49,6 +59,20 @@ const isShopifyFeatureEnabled = computed(
() => shopifyIntegration.value.enabled
);
const linearIntegration = useFunctionGetter(
'integrations/getIntegration',
'linear'
);
const isLinearIntegrationEnabled = computed(
() => linearIntegration.value?.enabled || false
);
const isLinearFeatureEnabled = isFeatureEnabledonAccount.value(
currentAccountId.value,
FEATURE_FLAGS.LINEAR
);
const store = useStore();
const currentChat = useMapGetter('getSelectedChat');
const conversationId = computed(() => props.conversationId);
@@ -103,6 +127,8 @@ onMounted(() => {
conversationSidebarItems.value = conversationSidebarItemsOrder.value;
getContactDetails();
store.dispatch('attributes/get', 0);
// Load integrations to ensure linear integration state is available
store.dispatch('integrations/get', 'linear');
});
</script>
@@ -113,7 +139,7 @@ onMounted(() => {
@close="closeContactPanel"
/>
<ContactInfo :contact="contact" :channel-type="channelType" />
<div class="list-group pb-8">
<div class="pb-8 list-group px-2">
<Draggable
:list="conversationSidebarItems"
animation="200"
@@ -125,140 +151,151 @@ onMounted(() => {
@end="onDragEnd"
>
<template #item="{ element }">
<div :key="element.name" class="px-2">
<div
v-if="element.name === 'conversation_actions'"
class="conversation--actions"
>
<AccordionItem
:title="
$t('CONVERSATION_SIDEBAR.ACCORDION.CONVERSATION_ACTIONS')
"
:is-open="isContactSidebarItemOpen('is_conv_actions_open')"
@toggle="
value => toggleSidebarUIState('is_conv_actions_open', value)
"
>
<ConversationAction
:conversation-id="conversationId"
:inbox-id="inboxId"
/>
</AccordionItem>
</div>
<div
v-else-if="element.name === 'conversation_participants'"
class="conversation--actions"
>
<AccordionItem
:title="$t('CONVERSATION_PARTICIPANTS.SIDEBAR_TITLE')"
:is-open="isContactSidebarItemOpen('is_conv_participants_open')"
@toggle="
value =>
toggleSidebarUIState('is_conv_participants_open', value)
"
>
<ConversationParticipant
:conversation-id="conversationId"
:inbox-id="inboxId"
/>
</AccordionItem>
</div>
<div v-else-if="element.name === 'conversation_info'">
<AccordionItem
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONVERSATION_INFO')"
:is-open="isContactSidebarItemOpen('is_conv_details_open')"
compact
@toggle="
value => toggleSidebarUIState('is_conv_details_open', value)
"
>
<ConversationInfo
:conversation-attributes="conversationAdditionalAttributes"
:contact-attributes="contactAdditionalAttributes"
/>
</AccordionItem>
</div>
<div v-else-if="element.name === 'contact_attributes'">
<AccordionItem
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONTACT_ATTRIBUTES')"
:is-open="
isContactSidebarItemOpen('is_contact_attributes_open')
"
compact
@toggle="
value =>
toggleSidebarUIState('is_contact_attributes_open', value)
"
>
<CustomAttributes
attribute-type="contact_attribute"
attribute-from="conversation_contact_panel"
:contact-id="contact.id"
:empty-state-message="
$t('CONVERSATION_CUSTOM_ATTRIBUTES.NO_RECORDS_FOUND')
"
/>
</AccordionItem>
</div>
<div v-else-if="element.name === 'previous_conversation'">
<AccordionItem
v-if="contact.id"
:title="
$t('CONVERSATION_SIDEBAR.ACCORDION.PREVIOUS_CONVERSATION')
"
:is-open="isContactSidebarItemOpen('is_previous_conv_open')"
compact
@toggle="
value => toggleSidebarUIState('is_previous_conv_open', value)
"
>
<ContactConversations
:contact-id="contact.id"
:conversation-id="conversationId"
/>
</AccordionItem>
</div>
<woot-feature-toggle
v-else-if="element.name === 'macros'"
feature-key="macros"
>
<AccordionItem
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.MACROS')"
:is-open="isContactSidebarItemOpen('is_macro_open')"
compact
@toggle="value => toggleSidebarUIState('is_macro_open', value)"
>
<MacrosList :conversation-id="conversationId" />
</AccordionItem>
</woot-feature-toggle>
<div
v-else-if="
element.name === 'shopify_orders' && isShopifyFeatureEnabled
<div
v-if="element.name === 'conversation_actions'"
class="conversation--actions"
>
<AccordionItem
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONVERSATION_ACTIONS')"
:is-open="isContactSidebarItemOpen('is_conv_actions_open')"
@toggle="
value => toggleSidebarUIState('is_conv_actions_open', value)
"
>
<AccordionItem
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.SHOPIFY_ORDERS')"
:is-open="isContactSidebarItemOpen('is_shopify_orders_open')"
compact
@toggle="
value => toggleSidebarUIState('is_shopify_orders_open', value)
<ConversationAction
:conversation-id="conversationId"
:inbox-id="inboxId"
/>
</AccordionItem>
</div>
<div
v-else-if="element.name === 'conversation_participants'"
class="conversation--actions"
>
<AccordionItem
:title="$t('CONVERSATION_PARTICIPANTS.SIDEBAR_TITLE')"
:is-open="isContactSidebarItemOpen('is_conv_participants_open')"
@toggle="
value =>
toggleSidebarUIState('is_conv_participants_open', value)
"
>
<ConversationParticipant
:conversation-id="conversationId"
:inbox-id="inboxId"
/>
</AccordionItem>
</div>
<div v-else-if="element.name === 'conversation_info'">
<AccordionItem
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONVERSATION_INFO')"
:is-open="isContactSidebarItemOpen('is_conv_details_open')"
compact
@toggle="
value => toggleSidebarUIState('is_conv_details_open', value)
"
>
<ConversationInfo
:conversation-attributes="conversationAdditionalAttributes"
:contact-attributes="contactAdditionalAttributes"
/>
</AccordionItem>
</div>
<div v-else-if="element.name === 'contact_attributes'">
<AccordionItem
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONTACT_ATTRIBUTES')"
:is-open="isContactSidebarItemOpen('is_contact_attributes_open')"
compact
@toggle="
value =>
toggleSidebarUIState('is_contact_attributes_open', value)
"
>
<CustomAttributes
attribute-type="contact_attribute"
attribute-from="conversation_contact_panel"
:contact-id="contact.id"
:empty-state-message="
$t('CONVERSATION_CUSTOM_ATTRIBUTES.NO_RECORDS_FOUND')
"
>
<ShopifyOrdersList :contact-id="contactId" />
</AccordionItem>
</div>
<div v-else-if="element.name === 'contact_notes'">
<AccordionItem
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONTACT_NOTES')"
:is-open="isContactSidebarItemOpen('is_contact_notes_open')"
compact
@toggle="
value => toggleSidebarUIState('is_contact_notes_open', value)
"
>
<ContactNotes :contact-id="contactId" />
</AccordionItem>
</div>
/>
</AccordionItem>
</div>
<div v-else-if="element.name === 'previous_conversation'">
<AccordionItem
v-if="contact.id"
:title="
$t('CONVERSATION_SIDEBAR.ACCORDION.PREVIOUS_CONVERSATION')
"
:is-open="isContactSidebarItemOpen('is_previous_conv_open')"
compact
@toggle="
value => toggleSidebarUIState('is_previous_conv_open', value)
"
>
<ContactConversations
:contact-id="contact.id"
:conversation-id="conversationId"
/>
</AccordionItem>
</div>
<woot-feature-toggle
v-else-if="element.name === 'macros'"
feature-key="macros"
>
<AccordionItem
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.MACROS')"
:is-open="isContactSidebarItemOpen('is_macro_open')"
compact
@toggle="value => toggleSidebarUIState('is_macro_open', value)"
>
<MacrosList :conversation-id="conversationId" />
</AccordionItem>
</woot-feature-toggle>
<div
v-else-if="
element.name === 'linear_issues' && isLinearFeatureEnabled
"
>
<AccordionItem
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.LINEAR_ISSUES')"
:is-open="isContactSidebarItemOpen('is_linear_issues_open')"
compact
@toggle="
value => toggleSidebarUIState('is_linear_issues_open', value)
"
>
<LinearSetupCTA v-if="!isLinearIntegrationEnabled" />
<LinearIssuesList v-else :conversation-id="conversationId" />
</AccordionItem>
</div>
<div
v-else-if="
element.name === 'shopify_orders' && isShopifyFeatureEnabled
"
>
<AccordionItem
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.SHOPIFY_ORDERS')"
:is-open="isContactSidebarItemOpen('is_shopify_orders_open')"
compact
@toggle="
value => toggleSidebarUIState('is_shopify_orders_open', value)
"
>
<ShopifyOrdersList :contact-id="contactId" />
</AccordionItem>
</div>
<div v-else-if="element.name === 'contact_notes'">
<AccordionItem
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONTACT_NOTES')"
:is-open="isContactSidebarItemOpen('is_contact_notes_open')"
compact
@toggle="
value => toggleSidebarUIState('is_contact_notes_open', value)
"
>
<ContactNotes :contact-id="contactId" />
</AccordionItem>
</div>
</template>
</Draggable>