mirror of
https://github.com/lingble/chatwoot.git
synced 2025-10-29 02:02:27 +00:00
This PR has the initial version of the new sidebar targeted for the next major redesign of the app. This PR includes the following changes - Components in the `layouts-next` and `base-next` directories in `dashboard/components` - Two generic components `Avatar` and `Icon` - `SidebarGroup` component to manage expandable sidebar groups with nested navigation items. This includes handling active states, transitions, and permissions. - `SidebarGroupHeader` component to display the header of each navigation group with optional icons and active state indication. - `SidebarGroupLeaf` component for individual navigation items within a group, supporting icons and active state. - `SidebarGroupSeparator` component to visually separate nested navigation items. (They look a lot like header) - `SidebarGroupEmptyLeaf` component to render empty state of any navigation groups. ---- Co-authored-by: Pranav <pranav@chatwoot.com> Co-authored-by: Pranav <pranavrajs@gmail.com>
474 lines
14 KiB
Vue
474 lines
14 KiB
Vue
<script setup>
|
|
import { h, 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 { useStorage } from '@vueuse/core';
|
|
|
|
import { useSidebarKeyboardShortcuts } from './useSidebarKeyboardShortcuts';
|
|
import SidebarGroup from './SidebarGroup.vue';
|
|
import SidebarProfileMenu from './SidebarProfileMenu.vue';
|
|
import ChannelLeaf from './ChannelLeaf.vue';
|
|
import SidebarNotificationBell from './SidebarNotificationBell.vue';
|
|
import SidebarAccountSwitcher from './SidebarAccountSwitcher.vue';
|
|
import Logo from 'next/icon/Logo.vue';
|
|
|
|
const emit = defineEmits([
|
|
'openNotificationPanel',
|
|
'closeKeyShortcutModal',
|
|
'openKeyShortcutModal',
|
|
'showCreateAccountModal',
|
|
]);
|
|
|
|
const { accountScopedRoute } = useAccount();
|
|
const store = useStore();
|
|
const searchShortcut = useKbd([`$mod`, 'k']);
|
|
const { t } = useI18n();
|
|
const enableNewConversation = false;
|
|
|
|
const toggleShortcutModalFn = show => {
|
|
if (show) {
|
|
emit('openKeyShortcutModal');
|
|
} else {
|
|
emit('closeKeyShortcutModal');
|
|
}
|
|
};
|
|
|
|
useSidebarKeyboardShortcuts(toggleShortcutModalFn);
|
|
|
|
// We're using localStorage to store the expanded item in the sidebar
|
|
// This helps preserve context when navigating between portal and dashboard layouts
|
|
// and also when the user refreshes the page
|
|
const expandedItem = useStorage(
|
|
'next-sidebar-expanded-item',
|
|
null,
|
|
localStorage
|
|
);
|
|
|
|
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 menuItems = computed(() => {
|
|
return [
|
|
{
|
|
name: 'Inbox',
|
|
label: t('SIDEBAR.INBOX'),
|
|
icon: 'i-lucide-inbox',
|
|
to: accountScopedRoute('inbox_view'),
|
|
},
|
|
{
|
|
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, { ...leafProps, 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-lucide-bot',
|
|
label: t('SIDEBAR.CAPTAIN'),
|
|
to: accountScopedRoute('captain'),
|
|
},
|
|
{
|
|
name: 'Contacts',
|
|
label: t('SIDEBAR.CONTACTS'),
|
|
icon: 'i-lucide-contact',
|
|
children: [
|
|
{
|
|
name: 'All Contacts',
|
|
label: t('SIDEBAR.ALL_CONTACTS'),
|
|
to: accountScopedRoute('contacts_dashboard'),
|
|
},
|
|
{
|
|
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_segments_dashboard', {
|
|
id: view.id,
|
|
}),
|
|
})),
|
|
},
|
|
{
|
|
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_labels_dashboard', {
|
|
label: label.title,
|
|
}),
|
|
})),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
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'),
|
|
},
|
|
{
|
|
name: 'Reports CSAT',
|
|
label: t('SIDEBAR.CSAT'),
|
|
to: accountScopedRoute('csat_reports'),
|
|
},
|
|
{
|
|
name: 'Reports Bot',
|
|
label: t('SIDEBAR.REPORTS_BOT'),
|
|
to: accountScopedRoute('bot_reports'),
|
|
},
|
|
{
|
|
name: 'Reports Agent',
|
|
label: t('SIDEBAR.REPORTS_AGENT'),
|
|
to: accountScopedRoute('agent_reports'),
|
|
},
|
|
{
|
|
name: 'Reports Label',
|
|
label: t('SIDEBAR.REPORTS_LABEL'),
|
|
to: accountScopedRoute('label_reports'),
|
|
},
|
|
{
|
|
name: 'Reports Inbox',
|
|
label: t('SIDEBAR.REPORTS_INBOX'),
|
|
to: accountScopedRoute('inbox_reports'),
|
|
},
|
|
{
|
|
name: 'Reports Team',
|
|
label: t('SIDEBAR.REPORTS_TEAM'),
|
|
to: accountScopedRoute('team_reports'),
|
|
},
|
|
{
|
|
name: 'Reports SLA',
|
|
label: t('SIDEBAR.REPORTS_SLA'),
|
|
to: accountScopedRoute('sla_reports'),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: 'Campaigns',
|
|
label: t('SIDEBAR.CAMPAIGNS'),
|
|
icon: 'i-lucide-megaphone',
|
|
children: [
|
|
{
|
|
name: 'Ongoing',
|
|
label: t('SIDEBAR.ONGOING'),
|
|
to: accountScopedRoute('ongoing_campaigns'),
|
|
},
|
|
{
|
|
name: 'One-off',
|
|
label: t('SIDEBAR.ONE_OFF'),
|
|
to: accountScopedRoute('one_off'),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: 'Portals',
|
|
label: t('SIDEBAR.HELP_CENTER.TITLE'),
|
|
icon: 'i-lucide-library-big',
|
|
to: accountScopedRoute('default_portal_articles'),
|
|
activeOn: [
|
|
'all_locale_categories',
|
|
'default_portal_articles',
|
|
'edit_article',
|
|
'edit_category',
|
|
'edit_portal_customization',
|
|
'edit_portal_information',
|
|
'edit_portal_locales',
|
|
'list_all_locale_articles',
|
|
'list_all_locale_categories',
|
|
'list_all_portals',
|
|
'list_archived_articles',
|
|
'list_draft_articles',
|
|
'list_mine_articles',
|
|
'new_article',
|
|
'new_category_in_locale',
|
|
'new_portal_information',
|
|
'portalSlug',
|
|
'portal_customization',
|
|
'portal_finish',
|
|
'show_category',
|
|
'show_category_articles',
|
|
],
|
|
},
|
|
{
|
|
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 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 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-workflow',
|
|
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: 'Settings Billing',
|
|
label: t('SIDEBAR.BILLING'),
|
|
icon: 'i-lucide-credit-card',
|
|
to: accountScopedRoute('billing_settings_index'),
|
|
},
|
|
],
|
|
},
|
|
];
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<aside
|
|
class="w-[200px] bg-n-solid-2 rtl:border-l ltr:border-r border-n-weak h-screen flex flex-col text-sm pt-2 pb-1"
|
|
>
|
|
<section class="grid gap-2 mt-2 mb-4">
|
|
<div class="flex gap-2 px-2 items-center min-w-0">
|
|
<div class="size-6 grid place-content-center flex-shrink-0">
|
|
<Logo />
|
|
</div>
|
|
<div class="w-px h-3 bg-n-strong flex-shrink-0" />
|
|
<SidebarAccountSwitcher
|
|
class="-mx-1 flex-grow min-w-0"
|
|
@show-create-account-modal="emit('showCreateAccountModal')"
|
|
/>
|
|
</div>
|
|
<div class="gap-2 flex px-2">
|
|
<RouterLink
|
|
:to="{ name: 'search' }"
|
|
class="rounded-lg py-1 flex items-center gap-2 px-2 border-n-weak border bg-n-solid-3 dark:bg-n-black/30 w-full"
|
|
>
|
|
<span class="i-lucide-search size-4 text-n-slate-11 flex-shrink-0" />
|
|
<span class="flex-grow text-left">
|
|
{{ t('COMBOBOX.SEARCH_PLACEHOLDER') }}
|
|
</span>
|
|
<span
|
|
class="tracking-wide select-none pointer-events-none text-n-slate-10 hidden"
|
|
>
|
|
{{ searchShortcut }}
|
|
</span>
|
|
</RouterLink>
|
|
<button
|
|
v-if="enableNewConversation"
|
|
class="rounded-lg py-1 flex items-center gap-2 px-2 border-n-weak border bg-n-solid-3 w-full"
|
|
>
|
|
<span
|
|
class="i-lucide-square-pen size-4 text-n-slate-11 flex-shrink-0"
|
|
/>
|
|
</button>
|
|
</div>
|
|
</section>
|
|
<nav class="grid gap-2 overflow-y-scroll no-scrollbar px-2 flex-grow pb-5">
|
|
<ul class="flex flex-col gap-2 list-none m-0">
|
|
<SidebarGroup
|
|
v-for="item in menuItems"
|
|
:key="item.name"
|
|
v-bind="item"
|
|
/>
|
|
</ul>
|
|
</nav>
|
|
<section
|
|
class="p-1 border-t border-n-strong shadow-[0px_-2px_4px_0px_rgba(27,28,29,0.02)] flex-shrink-0 flex justify-between gap-2 items-center"
|
|
>
|
|
<SidebarProfileMenu
|
|
@open-key-shortcut-modal="emit('openKeyShortcutModal')"
|
|
/>
|
|
<div v-if="false" class="flex items-center">
|
|
<div class="w-px h-3 bg-n-strong flex-shrink-0" />
|
|
<SidebarNotificationBell
|
|
@open-notification-panel="emit('openNotificationPanel')"
|
|
/>
|
|
</div>
|
|
</section>
|
|
</aside>
|
|
</template>
|