Files
leadchat/app/javascript/dashboard/components-next/sidebar/Sidebar.vue
Muhsin Keloth 04b2901e1f feat: Conversation workflows(EE) (#13040)
We are expanding Chatwoot’s automation capabilities by
introducing **Conversation Workflows**, a dedicated section in settings
where teams can configure rules that govern how conversations are closed
and what information agents must fill before resolving. This feature
helps teams enforce data consistency, collect structured resolution
information, and ensure downstream reporting is accurate.

Instead of having auto‑resolution buried inside Account Settings, we
introduced a new sidebar item:
- Auto‑resolve conversations (existing behaviour)
- Required attributes on resolution (new)

This groups all conversation‑closing logic into a single place.

#### Required Attributes on Resolve

Admins can now pick which custom conversation attributes must be filled
before an agent can resolve a conversation.

**How it works**

- Admin selects one or more attributes from the list of existing
conversation level custom attributes.
- These selected attributes become mandatory during resolution.
- List all the attributes configured via Required Attributes (Text,
Number, Link, Date, List, Checkbox)
- When an agent clicks Resolve Conversation:
If attributes already have values → the conversation resolves normally.
If attributes are missing → a modal appears prompting the agent to fill
them.

<img width="1554" height="1282" alt="CleanShot 2025-12-10 at 11 42
23@2x"
src="https://github.com/user-attachments/assets/4cd5d6e1-abe8-4999-accd-d4a08913b373"
/>


#### Custom Attributes Integration

On the Custom Attributes page, we will surfaced indicators showing how
each attribute is being used.

Each attribute will show badges such as:

- Resolution → used in the required‑on‑resolve workflow

- Pre‑chat form → already existing

<img width="2390" height="1822" alt="CleanShot 2025-12-10 at 11 43
42@2x"
src="https://github.com/user-attachments/assets/b92a6eb7-7f6c-40e6-bf23-6a5310f2d9c5"
/>


#### Admin Flow

- Navigate to Settings → Conversation Workflows.
- Under Required attributes on resolve, click Add Required Attribute.
- Pick from the dropdown list of conversation attributes.
- Save changes.

Agents will now be prompted automatically whenever they resolve.

<img width="2434" height="872" alt="CleanShot 2025-12-10 at 11 44 42@2x"
src="https://github.com/user-attachments/assets/632fc0e5-767c-4a1c-8cf4-ffe3d058d319"
/>



#### NOTES
- The Required Attributes on Resolve modal should only appear when
values are missing.
- Required attributes must block the resolution action until satisfied.
- Bulk‑resolve actions should follow the same rules — any conversation
missing attributes cannot be bulk‑resolved, rest will be resolved, show
a notification that the resolution cannot be done.
- API resolution does not respect the attributes.

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
2026-01-27 11:36:20 +04:00

682 lines
21 KiB
Vue

<script setup>
import { h, ref, computed, onMounted } from 'vue';
import { provideSidebarContext } from './provider';
import { useAccount } from 'dashboard/composables/useAccount';
import { useKbd } from 'dashboard/composables/utils/useKbd';
import { useMapGetter } from 'dashboard/composables/store';
import { useStore } from 'vuex';
import { useI18n } from 'vue-i18n';
import { useSidebarKeyboardShortcuts } from './useSidebarKeyboardShortcuts';
import { vOnClickOutside } from '@vueuse/components';
import { emitter } from 'shared/helpers/mitt';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import Button from 'dashboard/components-next/button/Button.vue';
import SidebarGroup from './SidebarGroup.vue';
import SidebarProfileMenu from './SidebarProfileMenu.vue';
import SidebarChangelogCard from './SidebarChangelogCard.vue';
import ChannelLeaf from './ChannelLeaf.vue';
import SidebarAccountSwitcher from './SidebarAccountSwitcher.vue';
import Logo from 'next/icon/Logo.vue';
import ComposeConversation from 'dashboard/components-next/NewConversation/ComposeConversation.vue';
const props = defineProps({
isMobileSidebarOpen: {
type: Boolean,
default: false,
},
});
const emit = defineEmits([
'closeKeyShortcutModal',
'openKeyShortcutModal',
'showCreateAccountModal',
'closeMobileSidebar',
]);
const { accountScopedRoute, isOnChatwootCloud } = useAccount();
const store = useStore();
const searchShortcut = useKbd([`$mod`, 'k']);
const { t } = useI18n();
const isACustomBrandedInstance = useMapGetter(
'globalConfig/isACustomBrandedInstance'
);
const toggleShortcutModalFn = show => {
if (show) {
emit('openKeyShortcutModal');
} else {
emit('closeKeyShortcutModal');
}
};
useSidebarKeyboardShortcuts(toggleShortcutModalFn);
const expandedItem = ref(null);
const setExpandedItem = name => {
expandedItem.value = expandedItem.value === name ? null : name;
};
provideSidebarContext({
expandedItem,
setExpandedItem,
});
const inboxes = useMapGetter('inboxes/getInboxes');
const labels = useMapGetter('labels/getLabelsOnSidebar');
const teams = useMapGetter('teams/getMyTeams');
const contactCustomViews = useMapGetter('customViews/getContactCustomViews');
const conversationCustomViews = useMapGetter(
'customViews/getConversationCustomViews'
);
onMounted(() => {
store.dispatch('labels/get');
store.dispatch('inboxes/get');
store.dispatch('notifications/unReadCount');
store.dispatch('teams/get');
store.dispatch('attributes/get');
store.dispatch('customViews/get', 'conversation');
store.dispatch('customViews/get', 'contact');
});
const sortedInboxes = computed(() =>
inboxes.value.slice().sort((a, b) => a.name.localeCompare(b.name))
);
const closeMobileSidebar = () => {
if (!props.isMobileSidebarOpen) return;
emit('closeMobileSidebar');
};
const onComposeOpen = toggleFn => {
toggleFn();
emitter.emit(BUS_EVENTS.NEW_CONVERSATION_MODAL, true);
};
const onComposeClose = () => {
emitter.emit(BUS_EVENTS.NEW_CONVERSATION_MODAL, false);
};
const newReportRoutes = () => [
{
name: 'Reports Agent',
label: t('SIDEBAR.REPORTS_AGENT'),
to: accountScopedRoute('agent_reports_index'),
activeOn: ['agent_reports_show'],
},
{
name: 'Reports Label',
label: t('SIDEBAR.REPORTS_LABEL'),
to: accountScopedRoute('label_reports_index'),
},
{
name: 'Reports Inbox',
label: t('SIDEBAR.REPORTS_INBOX'),
to: accountScopedRoute('inbox_reports_index'),
activeOn: ['inbox_reports_show'],
},
{
name: 'Reports Team',
label: t('SIDEBAR.REPORTS_TEAM'),
to: accountScopedRoute('team_reports_index'),
activeOn: ['team_reports_show'],
},
];
const reportRoutes = computed(() => newReportRoutes());
const menuItems = computed(() => {
return [
{
name: 'Inbox',
label: t('SIDEBAR.INBOX'),
icon: 'i-lucide-inbox',
to: accountScopedRoute('inbox_view'),
activeOn: ['inbox_view', 'inbox_view_conversation'],
getterKeys: {
count: 'notifications/getUnreadCount',
},
},
{
name: 'Conversation',
label: t('SIDEBAR.CONVERSATIONS'),
icon: 'i-lucide-message-circle',
children: [
{
name: 'All',
label: t('SIDEBAR.ALL_CONVERSATIONS'),
activeOn: ['inbox_conversation'],
to: accountScopedRoute('home'),
},
{
name: 'Mentions',
label: t('SIDEBAR.MENTIONED_CONVERSATIONS'),
activeOn: ['conversation_through_mentions'],
to: accountScopedRoute('conversation_mentions'),
},
{
name: 'Unattended',
activeOn: ['conversation_through_unattended'],
label: t('SIDEBAR.UNATTENDED_CONVERSATIONS'),
to: accountScopedRoute('conversation_unattended'),
},
{
name: 'Folders',
label: t('SIDEBAR.CUSTOM_VIEWS_FOLDER'),
icon: 'i-lucide-folder',
activeOn: ['conversations_through_folders'],
children: conversationCustomViews.value.map(view => ({
name: `${view.name}-${view.id}`,
label: view.name,
to: accountScopedRoute('folder_conversations', { id: view.id }),
})),
},
{
name: 'Teams',
label: t('SIDEBAR.TEAMS'),
icon: 'i-lucide-users',
activeOn: ['conversations_through_team'],
children: teams.value.map(team => ({
name: `${team.name}-${team.id}`,
label: team.name,
to: accountScopedRoute('team_conversations', { teamId: team.id }),
})),
},
{
name: 'Channels',
label: t('SIDEBAR.CHANNELS'),
icon: 'i-lucide-mailbox',
activeOn: ['conversation_through_inbox'],
children: sortedInboxes.value.map(inbox => ({
name: `${inbox.name}-${inbox.id}`,
label: inbox.name,
to: accountScopedRoute('inbox_dashboard', { inbox_id: inbox.id }),
component: leafProps =>
h(ChannelLeaf, {
label: leafProps.label,
active: leafProps.active,
inbox,
}),
})),
},
{
name: 'Labels',
label: t('SIDEBAR.LABELS'),
icon: 'i-lucide-tag',
activeOn: ['conversations_through_label'],
children: labels.value.map(label => ({
name: `${label.title}-${label.id}`,
label: label.title,
icon: h('span', {
class: `size-[12px] ring-1 ring-n-alpha-1 dark:ring-white/20 ring-inset rounded-sm`,
style: { backgroundColor: label.color },
}),
to: accountScopedRoute('label_conversations', {
label: label.title,
}),
})),
},
],
},
{
name: 'Captain',
icon: 'i-woot-captain',
label: t('SIDEBAR.CAPTAIN'),
activeOn: ['captain_assistants_create_index'],
children: [
{
name: 'FAQs',
label: t('SIDEBAR.CAPTAIN_RESPONSES'),
activeOn: [
'captain_assistants_responses_index',
'captain_assistants_responses_pending',
],
to: accountScopedRoute('captain_assistants_index', {
navigationPath: 'captain_assistants_responses_index',
}),
},
{
name: 'Documents',
label: t('SIDEBAR.CAPTAIN_DOCUMENTS'),
activeOn: ['captain_assistants_documents_index'],
to: accountScopedRoute('captain_assistants_index', {
navigationPath: 'captain_assistants_documents_index',
}),
},
{
name: 'Scenarios',
label: t('SIDEBAR.CAPTAIN_SCENARIOS'),
activeOn: ['captain_assistants_scenarios_index'],
to: accountScopedRoute('captain_assistants_index', {
navigationPath: 'captain_assistants_scenarios_index',
}),
},
{
name: 'Playground',
label: t('SIDEBAR.CAPTAIN_PLAYGROUND'),
activeOn: ['captain_assistants_playground_index'],
to: accountScopedRoute('captain_assistants_index', {
navigationPath: 'captain_assistants_playground_index',
}),
},
{
name: 'Inboxes',
label: t('SIDEBAR.CAPTAIN_INBOXES'),
activeOn: ['captain_assistants_inboxes_index'],
to: accountScopedRoute('captain_assistants_index', {
navigationPath: 'captain_assistants_inboxes_index',
}),
},
{
name: 'Tools',
label: t('SIDEBAR.CAPTAIN_TOOLS'),
activeOn: ['captain_tools_index'],
to: accountScopedRoute('captain_assistants_index', {
navigationPath: 'captain_tools_index',
}),
},
{
name: 'Settings',
label: t('SIDEBAR.CAPTAIN_SETTINGS'),
activeOn: [
'captain_assistants_settings_index',
'captain_assistants_guidelines_index',
'captain_assistants_guardrails_index',
],
to: accountScopedRoute('captain_assistants_index', {
navigationPath: 'captain_assistants_settings_index',
}),
},
],
},
{
name: 'Contacts',
label: t('SIDEBAR.CONTACTS'),
icon: 'i-lucide-contact',
children: [
{
name: 'All Contacts',
label: t('SIDEBAR.ALL_CONTACTS'),
to: accountScopedRoute(
'contacts_dashboard_index',
{},
{ page: 1, search: undefined }
),
activeOn: ['contacts_dashboard_index', 'contacts_edit'],
},
{
name: 'Active',
label: t('SIDEBAR.ACTIVE'),
to: accountScopedRoute('contacts_dashboard_active'),
activeOn: ['contacts_dashboard_active'],
},
{
name: 'Segments',
icon: 'i-lucide-group',
label: t('SIDEBAR.CUSTOM_VIEWS_SEGMENTS'),
children: contactCustomViews.value.map(view => ({
name: `${view.name}-${view.id}`,
label: view.name,
to: accountScopedRoute(
'contacts_dashboard_segments_index',
{ segmentId: view.id },
{ page: 1 }
),
activeOn: [
'contacts_dashboard_segments_index',
'contacts_edit_segment',
],
})),
},
{
name: 'Tagged With',
icon: 'i-lucide-tag',
label: t('SIDEBAR.TAGGED_WITH'),
children: labels.value.map(label => ({
name: `${label.title}-${label.id}`,
label: label.title,
icon: h('span', {
class: `size-[12px] ring-1 ring-n-alpha-1 dark:ring-white/20 ring-inset rounded-sm`,
style: { backgroundColor: label.color },
}),
to: accountScopedRoute(
'contacts_dashboard_labels_index',
{ label: label.title },
{ page: 1, search: undefined }
),
activeOn: [
'contacts_dashboard_labels_index',
'contacts_edit_label',
],
})),
},
],
},
{
name: 'Companies',
label: t('SIDEBAR.COMPANIES'),
icon: 'i-lucide-building-2',
children: [
{
name: 'All Companies',
label: t('SIDEBAR.ALL_COMPANIES'),
to: accountScopedRoute(
'companies_dashboard_index',
{},
{ page: 1, search: undefined }
),
activeOn: ['companies_dashboard_index'],
},
],
},
{
name: 'Reports',
label: t('SIDEBAR.REPORTS'),
icon: 'i-lucide-chart-spline',
children: [
{
name: 'Report Overview',
label: t('SIDEBAR.REPORTS_OVERVIEW'),
to: accountScopedRoute('account_overview_reports'),
},
{
name: 'Report Conversation',
label: t('SIDEBAR.REPORTS_CONVERSATION'),
to: accountScopedRoute('conversation_reports'),
},
...reportRoutes.value,
{
name: 'Reports CSAT',
label: t('SIDEBAR.CSAT'),
to: accountScopedRoute('csat_reports'),
},
{
name: 'Reports SLA',
label: t('SIDEBAR.REPORTS_SLA'),
to: accountScopedRoute('sla_reports'),
},
{
name: 'Reports Bot',
label: t('SIDEBAR.REPORTS_BOT'),
to: accountScopedRoute('bot_reports'),
},
],
},
{
name: 'Campaigns',
label: t('SIDEBAR.CAMPAIGNS'),
icon: 'i-lucide-megaphone',
children: [
{
name: 'Live chat',
label: t('SIDEBAR.LIVE_CHAT'),
to: accountScopedRoute('campaigns_livechat_index'),
},
{
name: 'SMS',
label: t('SIDEBAR.SMS'),
to: accountScopedRoute('campaigns_sms_index'),
},
{
name: 'WhatsApp',
label: t('SIDEBAR.WHATSAPP'),
to: accountScopedRoute('campaigns_whatsapp_index'),
},
],
},
{
name: 'Portals',
label: t('SIDEBAR.HELP_CENTER.TITLE'),
icon: 'i-lucide-library-big',
children: [
{
name: 'Articles',
label: t('SIDEBAR.HELP_CENTER.ARTICLES'),
activeOn: [
'portals_articles_index',
'portals_articles_new',
'portals_articles_edit',
],
to: accountScopedRoute('portals_index', {
navigationPath: 'portals_articles_index',
}),
},
{
name: 'Categories',
label: t('SIDEBAR.HELP_CENTER.CATEGORIES'),
activeOn: [
'portals_categories_index',
'portals_categories_articles_index',
'portals_categories_articles_edit',
],
to: accountScopedRoute('portals_index', {
navigationPath: 'portals_categories_index',
}),
},
{
name: 'Locales',
label: t('SIDEBAR.HELP_CENTER.LOCALES'),
activeOn: ['portals_locales_index'],
to: accountScopedRoute('portals_index', {
navigationPath: 'portals_locales_index',
}),
},
{
name: 'Settings',
label: t('SIDEBAR.HELP_CENTER.SETTINGS'),
activeOn: ['portals_settings_index'],
to: accountScopedRoute('portals_index', {
navigationPath: 'portals_settings_index',
}),
},
],
},
{
name: 'Settings',
label: t('SIDEBAR.SETTINGS'),
icon: 'i-lucide-bolt',
children: [
{
name: 'Settings Account Settings',
label: t('SIDEBAR.ACCOUNT_SETTINGS'),
icon: 'i-lucide-briefcase',
to: accountScopedRoute('general_settings_index'),
},
// {
// name: 'Settings Captain',
// label: t('SIDEBAR.CAPTAIN_AI'),
// icon: 'i-woot-captain',
// to: accountScopedRoute('captain_settings_index'),
// },
{
name: 'Settings Agents',
label: t('SIDEBAR.AGENTS'),
icon: 'i-lucide-square-user',
to: accountScopedRoute('agent_list'),
},
{
name: 'Settings Teams',
label: t('SIDEBAR.TEAMS'),
icon: 'i-lucide-users',
to: accountScopedRoute('settings_teams_list'),
},
{
name: 'Settings Agent Assignment',
label: t('SIDEBAR.AGENT_ASSIGNMENT'),
icon: 'i-lucide-user-cog',
to: accountScopedRoute('assignment_policy_index'),
},
{
name: 'Settings Inboxes',
label: t('SIDEBAR.INBOXES'),
icon: 'i-lucide-inbox',
to: accountScopedRoute('settings_inbox_list'),
},
{
name: 'Settings Labels',
label: t('SIDEBAR.LABELS'),
icon: 'i-lucide-tags',
to: accountScopedRoute('labels_list'),
},
{
name: 'Settings Custom Attributes',
label: t('SIDEBAR.CUSTOM_ATTRIBUTES'),
icon: 'i-lucide-code',
to: accountScopedRoute('attributes_list'),
},
{
name: 'Settings Automation',
label: t('SIDEBAR.AUTOMATION'),
icon: 'i-lucide-repeat',
to: accountScopedRoute('automation_list'),
},
{
name: 'Settings Agent Bots',
label: t('SIDEBAR.AGENT_BOTS'),
icon: 'i-lucide-bot',
to: accountScopedRoute('agent_bots'),
},
{
name: 'Settings Macros',
label: t('SIDEBAR.MACROS'),
icon: 'i-lucide-toy-brick',
to: accountScopedRoute('macros_wrapper'),
},
{
name: 'Settings Canned Responses',
label: t('SIDEBAR.CANNED_RESPONSES'),
icon: 'i-lucide-message-square-quote',
to: accountScopedRoute('canned_list'),
},
{
name: 'Settings Integrations',
label: t('SIDEBAR.INTEGRATIONS'),
icon: 'i-lucide-blocks',
to: accountScopedRoute('settings_applications'),
},
{
name: 'Settings Audit Logs',
label: t('SIDEBAR.AUDIT_LOGS'),
icon: 'i-lucide-briefcase',
to: accountScopedRoute('auditlogs_list'),
},
{
name: 'Settings Custom Roles',
label: t('SIDEBAR.CUSTOM_ROLES'),
icon: 'i-lucide-shield-plus',
to: accountScopedRoute('custom_roles_list'),
},
{
name: 'Settings Sla',
label: t('SIDEBAR.SLA'),
icon: 'i-lucide-clock-alert',
to: accountScopedRoute('sla_list'),
},
{
name: 'Conversation Workflow',
label: t('SIDEBAR.CONVERSATION_WORKFLOW'),
icon: 'i-lucide-workflow',
to: accountScopedRoute('conversation_workflow_index'),
},
{
name: 'Settings Security',
label: t('SIDEBAR.SECURITY'),
icon: 'i-lucide-shield',
to: accountScopedRoute('security_settings_index'),
},
{
name: 'Settings Billing',
label: t('SIDEBAR.BILLING'),
icon: 'i-lucide-credit-card',
to: accountScopedRoute('billing_settings_index'),
},
],
},
];
});
</script>
<template>
<aside
v-on-click-outside="[
closeMobileSidebar,
{ ignore: ['#mobile-sidebar-launcher'] },
]"
class="bg-n-solid-2 rtl:border-l ltr:border-r border-n-weak flex flex-col text-sm pb-1 fixed top-0 ltr:left-0 rtl:right-0 h-full z-40 transition-transform duration-200 ease-in-out md:static w-[200px] basis-[200px] md:flex-shrink-0 md:ltr:translate-x-0 md:rtl:-translate-x-0"
:class="[
{
'shadow-lg md:shadow-none': isMobileSidebarOpen,
'ltr:-translate-x-full rtl:translate-x-full': !isMobileSidebarOpen,
},
]"
>
<section class="grid gap-2 mt-2 mb-4">
<div class="flex gap-2 items-center px-2 min-w-0">
<div class="grid flex-shrink-0 place-content-center size-6">
<Logo class="size-4" />
</div>
<div class="flex-shrink-0 w-px h-3 bg-n-strong" />
<SidebarAccountSwitcher
class="flex-grow -mx-1 min-w-0"
@show-create-account-modal="emit('showCreateAccountModal')"
/>
</div>
<div class="flex gap-2 px-2">
<RouterLink
:to="{ name: 'search' }"
class="flex gap-2 items-center px-2 py-1 w-full h-7 rounded-lg outline outline-1 outline-n-weak bg-n-solid-3 dark:bg-n-black/30"
>
<span class="flex-shrink-0 i-lucide-search size-4 text-n-slate-11" />
<span class="flex-grow text-left">
{{ t('COMBOBOX.SEARCH_PLACEHOLDER') }}
</span>
<span
class="hidden tracking-wide pointer-events-none select-none text-n-slate-10"
>
{{ searchShortcut }}
</span>
</RouterLink>
<ComposeConversation align-position="right" @close="onComposeClose">
<template #trigger="{ toggle }">
<Button
icon="i-lucide-pen-line"
color="slate"
size="sm"
class="!h-7 !bg-n-solid-3 dark:!bg-n-black/30 !outline-n-weak !text-n-slate-11"
@click="onComposeOpen(toggle)"
/>
</template>
</ComposeConversation>
</div>
</section>
<nav class="grid overflow-y-scroll flex-grow gap-2 px-2 pb-5 no-scrollbar">
<ul class="flex flex-col gap-1.5 m-0 list-none">
<SidebarGroup
v-for="item in menuItems"
:key="item.name"
v-bind="item"
/>
</ul>
</nav>
<section
class="flex relative flex-col flex-shrink-0 gap-1 justify-between items-center"
>
<div
class="pointer-events-none absolute inset-x-0 -top-[31px] h-8 bg-gradient-to-t from-n-solid-2 to-transparent"
/>
<SidebarChangelogCard
v-if="isOnChatwootCloud && !isACustomBrandedInstance"
/>
<div
class="p-1 flex-shrink-0 flex w-full justify-between z-10 gap-2 items-center border-t border-n-weak shadow-[0px_-2px_4px_0px_rgba(27,28,29,0.02)]"
>
<SidebarProfileMenu
@open-key-shortcut-modal="emit('openKeyShortcutModal')"
/>
</div>
</section>
</aside>
</template>