From 6d3ecfe3c1d3bc860ba1fc050ebc3319cc5a8787 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 24 Oct 2024 07:02:37 +0530 Subject: [PATCH] feat: Add new sidebar for Chatwoot V4 (#10291) 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 Co-authored-by: Pranav --- .../dashboard/assets/scss/_woot.scss | 12 + .../components-next/avatar/Avatar.story.vue | 38 +- .../components-next/avatar/Avatar.vue | 130 +++-- .../components-next/button/Button.vue | 6 +- .../dashboard/components-next/icon/Icon.vue | 19 + .../dashboard/components-next/icon/Logo.vue | 28 ++ .../components-next/sidebar/ChannelLeaf.vue | 71 +++ .../components-next/sidebar/Sidebar.vue | 473 ++++++++++++++++++ .../sidebar/SidebarAccountSwitcher.vue | 110 ++++ .../components-next/sidebar/SidebarGroup.vue | 146 ++++++ .../sidebar/SidebarGroupEmptyLeaf.vue | 13 + .../sidebar/SidebarGroupHeader.vue | 42 ++ .../sidebar/SidebarGroupLeaf.vue | 92 ++++ .../sidebar/SidebarGroupSeparator.vue | 25 + .../sidebar/SidebarNotificationBell.vue | 40 ++ .../sidebar/SidebarProfileMenu.vue | 146 ++++++ .../sidebar/SidebarProfileMenuStatus.vue | 119 +++++ .../sidebar/SidebarSubGroup.vue | 114 +++++ .../components-next/sidebar/provider.js | 49 ++ .../sidebar/useSidebarKeyboardShortcuts.js | 39 ++ .../dashboard/components/ChatList.vue | 2 +- .../dashboard/components/SidemenuIcon.vue | 16 + .../dashboard/components/layout/Sidebar.vue | 3 + .../dashboard/components/policy.vue | 31 +- .../components/specs/SidemenuIcon.spec.js | 19 + .../composables/spec/useAccount.spec.js | 92 +++- .../dashboard/composables/useAccount.js | 15 +- .../dashboard/composables/usePolicy.js | 29 ++ .../dashboard/composables/utils/useKbd.js | 22 + app/javascript/dashboard/featureFlags.js | 1 + .../dashboard/i18n/locale/en/settings.json | 5 +- .../dashboard/routes/dashboard/Dashboard.vue | 31 +- .../contacts/components/ContactsView.vue | 2 +- .../routes/dashboard/dashboard.routes.js | 3 + .../components/HelpCenterLayout.vue | 16 + .../routes/dashboard/inbox/routes.js | 4 + .../settings/reports/reports.routes.js | 3 + .../dashboard/store/modules/customViews.js | 62 ++- .../modules/specs/customViews/actions.spec.js | 33 +- .../modules/specs/customViews/fixtures.js | 25 +- .../modules/specs/customViews/getters.spec.js | 44 +- .../specs/customViews/mutations.spec.js | 120 ++++- config/features.yml | 2 + package.json | 1 + pnpm-lock.yaml | 10 + tailwind.config.js | 39 +- vite.config.ts | 1 + 47 files changed, 2188 insertions(+), 155 deletions(-) create mode 100644 app/javascript/dashboard/components-next/icon/Icon.vue create mode 100644 app/javascript/dashboard/components-next/icon/Logo.vue create mode 100644 app/javascript/dashboard/components-next/sidebar/ChannelLeaf.vue create mode 100644 app/javascript/dashboard/components-next/sidebar/Sidebar.vue create mode 100644 app/javascript/dashboard/components-next/sidebar/SidebarAccountSwitcher.vue create mode 100644 app/javascript/dashboard/components-next/sidebar/SidebarGroup.vue create mode 100644 app/javascript/dashboard/components-next/sidebar/SidebarGroupEmptyLeaf.vue create mode 100644 app/javascript/dashboard/components-next/sidebar/SidebarGroupHeader.vue create mode 100644 app/javascript/dashboard/components-next/sidebar/SidebarGroupLeaf.vue create mode 100644 app/javascript/dashboard/components-next/sidebar/SidebarGroupSeparator.vue create mode 100644 app/javascript/dashboard/components-next/sidebar/SidebarNotificationBell.vue create mode 100644 app/javascript/dashboard/components-next/sidebar/SidebarProfileMenu.vue create mode 100644 app/javascript/dashboard/components-next/sidebar/SidebarProfileMenuStatus.vue create mode 100644 app/javascript/dashboard/components-next/sidebar/SidebarSubGroup.vue create mode 100644 app/javascript/dashboard/components-next/sidebar/provider.js create mode 100644 app/javascript/dashboard/components-next/sidebar/useSidebarKeyboardShortcuts.js create mode 100644 app/javascript/dashboard/composables/usePolicy.js create mode 100644 app/javascript/dashboard/composables/utils/useKbd.js diff --git a/app/javascript/dashboard/assets/scss/_woot.scss b/app/javascript/dashboard/assets/scss/_woot.scss index 8afefe384..75df32cfe 100644 --- a/app/javascript/dashboard/assets/scss/_woot.scss +++ b/app/javascript/dashboard/assets/scss/_woot.scss @@ -540,3 +540,15 @@ --color-orange-900: 255 224 194; } } + +@layer utilities { + /* Hide scrollbar for Chrome, Safari and Opera */ + .no-scrollbar::-webkit-scrollbar { + display: none; + } + /* Hide scrollbar for IE, Edge and Firefox */ + .no-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } +} diff --git a/app/javascript/dashboard/components-next/avatar/Avatar.story.vue b/app/javascript/dashboard/components-next/avatar/Avatar.story.vue index 869970f94..d502291c9 100644 --- a/app/javascript/dashboard/components-next/avatar/Avatar.story.vue +++ b/app/javascript/dashboard/components-next/avatar/Avatar.story.vue @@ -7,28 +7,58 @@ import Avatar from './Avatar.vue';
+ +
+ +
+
+ + +
+ + +
+
+ + +
+ +
+
+
diff --git a/app/javascript/dashboard/components-next/avatar/Avatar.vue b/app/javascript/dashboard/components-next/avatar/Avatar.vue index d5bb653b2..d558f030a 100644 --- a/app/javascript/dashboard/components-next/avatar/Avatar.vue +++ b/app/javascript/dashboard/components-next/avatar/Avatar.vue @@ -1,52 +1,122 @@ diff --git a/app/javascript/dashboard/components-next/button/Button.vue b/app/javascript/dashboard/components-next/button/Button.vue index 5f7e8d6c7..d2487cabe 100644 --- a/app/javascript/dashboard/components-next/button/Button.vue +++ b/app/javascript/dashboard/components-next/button/Button.vue @@ -118,7 +118,11 @@ const handleClick = () => { :icon-lib="iconLib" class="flex-shrink-0" /> - {{ label }} + + + {{ label }} + + +import { h, isVNode } from 'vue'; + +const props = defineProps({ + icon: { type: [String, Object, Function], required: true }, +}); + +const renderIcon = () => { + if (!props.icon) return null; + if (typeof props.icon === 'function' || isVNode(props.icon)) { + return props.icon; + } + return h('span', { class: props.icon }); +}; + + + diff --git a/app/javascript/dashboard/components-next/icon/Logo.vue b/app/javascript/dashboard/components-next/icon/Logo.vue new file mode 100644 index 000000000..2330021c3 --- /dev/null +++ b/app/javascript/dashboard/components-next/icon/Logo.vue @@ -0,0 +1,28 @@ + diff --git a/app/javascript/dashboard/components-next/sidebar/ChannelLeaf.vue b/app/javascript/dashboard/components-next/sidebar/ChannelLeaf.vue new file mode 100644 index 000000000..2ae5bd0c5 --- /dev/null +++ b/app/javascript/dashboard/components-next/sidebar/ChannelLeaf.vue @@ -0,0 +1,71 @@ + + + diff --git a/app/javascript/dashboard/components-next/sidebar/Sidebar.vue b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue new file mode 100644 index 000000000..a84db8ae5 --- /dev/null +++ b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue @@ -0,0 +1,473 @@ + + + diff --git a/app/javascript/dashboard/components-next/sidebar/SidebarAccountSwitcher.vue b/app/javascript/dashboard/components-next/sidebar/SidebarAccountSwitcher.vue new file mode 100644 index 000000000..881034b4c --- /dev/null +++ b/app/javascript/dashboard/components-next/sidebar/SidebarAccountSwitcher.vue @@ -0,0 +1,110 @@ + + + diff --git a/app/javascript/dashboard/components-next/sidebar/SidebarGroup.vue b/app/javascript/dashboard/components-next/sidebar/SidebarGroup.vue new file mode 100644 index 000000000..ecc189bea --- /dev/null +++ b/app/javascript/dashboard/components-next/sidebar/SidebarGroup.vue @@ -0,0 +1,146 @@ + + + + diff --git a/app/javascript/dashboard/components-next/sidebar/SidebarGroupEmptyLeaf.vue b/app/javascript/dashboard/components-next/sidebar/SidebarGroupEmptyLeaf.vue new file mode 100644 index 000000000..dd4128dfe --- /dev/null +++ b/app/javascript/dashboard/components-next/sidebar/SidebarGroupEmptyLeaf.vue @@ -0,0 +1,13 @@ + + + diff --git a/app/javascript/dashboard/components-next/sidebar/SidebarGroupHeader.vue b/app/javascript/dashboard/components-next/sidebar/SidebarGroupHeader.vue new file mode 100644 index 000000000..289ec73a4 --- /dev/null +++ b/app/javascript/dashboard/components-next/sidebar/SidebarGroupHeader.vue @@ -0,0 +1,42 @@ + + + diff --git a/app/javascript/dashboard/components-next/sidebar/SidebarGroupLeaf.vue b/app/javascript/dashboard/components-next/sidebar/SidebarGroupLeaf.vue new file mode 100644 index 000000000..13c893fa5 --- /dev/null +++ b/app/javascript/dashboard/components-next/sidebar/SidebarGroupLeaf.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/app/javascript/dashboard/components-next/sidebar/SidebarGroupSeparator.vue b/app/javascript/dashboard/components-next/sidebar/SidebarGroupSeparator.vue new file mode 100644 index 000000000..ddc92a2b3 --- /dev/null +++ b/app/javascript/dashboard/components-next/sidebar/SidebarGroupSeparator.vue @@ -0,0 +1,25 @@ + + + diff --git a/app/javascript/dashboard/components-next/sidebar/SidebarNotificationBell.vue b/app/javascript/dashboard/components-next/sidebar/SidebarNotificationBell.vue new file mode 100644 index 000000000..94f3dd6bd --- /dev/null +++ b/app/javascript/dashboard/components-next/sidebar/SidebarNotificationBell.vue @@ -0,0 +1,40 @@ + + + diff --git a/app/javascript/dashboard/components-next/sidebar/SidebarProfileMenu.vue b/app/javascript/dashboard/components-next/sidebar/SidebarProfileMenu.vue new file mode 100644 index 000000000..ae88ebe69 --- /dev/null +++ b/app/javascript/dashboard/components-next/sidebar/SidebarProfileMenu.vue @@ -0,0 +1,146 @@ + + + diff --git a/app/javascript/dashboard/components-next/sidebar/SidebarProfileMenuStatus.vue b/app/javascript/dashboard/components-next/sidebar/SidebarProfileMenuStatus.vue new file mode 100644 index 000000000..9642e4103 --- /dev/null +++ b/app/javascript/dashboard/components-next/sidebar/SidebarProfileMenuStatus.vue @@ -0,0 +1,119 @@ + + + diff --git a/app/javascript/dashboard/components-next/sidebar/SidebarSubGroup.vue b/app/javascript/dashboard/components-next/sidebar/SidebarSubGroup.vue new file mode 100644 index 000000000..8c7f5c3a7 --- /dev/null +++ b/app/javascript/dashboard/components-next/sidebar/SidebarSubGroup.vue @@ -0,0 +1,114 @@ + + + diff --git a/app/javascript/dashboard/components-next/sidebar/provider.js b/app/javascript/dashboard/components-next/sidebar/provider.js new file mode 100644 index 000000000..d6571dcd9 --- /dev/null +++ b/app/javascript/dashboard/components-next/sidebar/provider.js @@ -0,0 +1,49 @@ +import { inject, provide } from 'vue'; +import { usePolicy } from 'dashboard/composables/usePolicy'; +import { useRouter } from 'vue-router'; + +const SidebarControl = Symbol('SidebarControl'); + +export function useSidebarContext() { + const context = inject(SidebarControl, null); + if (context === null) { + throw new Error(`Component is missing a parent component.`); + } + + const router = useRouter(); + const { checkFeatureAllowed, checkPermissions } = usePolicy(); + + const resolvePath = to => { + if (to) return router.resolve(to)?.path || '/'; + return '/'; + }; + + const resolvePermissions = to => { + if (to) return router.resolve(to)?.meta?.permissions ?? []; + return []; + }; + + const resolveFeatureFlag = to => { + if (to) return router.resolve(to)?.meta?.featureFlag || ''; + return ''; + }; + + const isAllowed = to => { + const permissions = resolvePermissions(to); + const featureFlag = resolveFeatureFlag(to); + + return checkPermissions(permissions) && checkFeatureAllowed(featureFlag); + }; + + return { + ...context, + resolvePath, + resolvePermissions, + resolveFeatureFlag, + isAllowed, + }; +} + +export function provideSidebarContext(context) { + provide(SidebarControl, context); +} diff --git a/app/javascript/dashboard/components-next/sidebar/useSidebarKeyboardShortcuts.js b/app/javascript/dashboard/components-next/sidebar/useSidebarKeyboardShortcuts.js new file mode 100644 index 000000000..0ef050369 --- /dev/null +++ b/app/javascript/dashboard/components-next/sidebar/useSidebarKeyboardShortcuts.js @@ -0,0 +1,39 @@ +import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents'; +import { useRoute, useRouter } from 'vue-router'; + +export function useSidebarKeyboardShortcuts(toggleShortcutModalFn) { + const route = useRoute(); + const router = useRouter(); + + const isCurrentRouteSameAsNavigation = routeName => { + return route.name === routeName; + }; + + const navigateToRoute = routeName => { + if (!isCurrentRouteSameAsNavigation(routeName)) { + router.push({ name: routeName }); + } + }; + const keyboardEvents = { + '$mod+Slash': { + action: () => toggleShortcutModalFn(true), + }, + '$mod+Escape': { + action: () => toggleShortcutModalFn(false), + }, + 'Alt+KeyC': { + action: () => navigateToRoute('home'), + }, + 'Alt+KeyV': { + action: () => navigateToRoute('contacts_dashboard'), + }, + 'Alt+KeyR': { + action: () => navigateToRoute('account_overview_reports'), + }, + 'Alt+KeyS': { + action: () => navigateToRoute('agent_list'), + }, + }; + + return useKeyboardEvents(keyboardEvents); +} diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue index 267b9e422..3ebefebe6 100644 --- a/app/javascript/dashboard/components/ChatList.vue +++ b/app/javascript/dashboard/components/ChatList.vue @@ -109,7 +109,7 @@ const chatListLoading = useMapGetter('getChatListLoadingStatus'); const activeInbox = useMapGetter('getSelectedInbox'); const conversationStats = useMapGetter('conversationStats/getStats'); const appliedFilters = useMapGetter('getAppliedConversationFilters'); -const folders = useMapGetter('customViews/getCustomViews'); +const folders = useMapGetter('customViews/getConversationCustomViews'); const agentList = useMapGetter('agents/getAgents'); const teamsList = useMapGetter('teams/getTeams'); const inboxesList = useMapGetter('inboxes/getInboxes'); diff --git a/app/javascript/dashboard/components/SidemenuIcon.vue b/app/javascript/dashboard/components/SidemenuIcon.vue index a81a3fedb..59ef4c4c2 100644 --- a/app/javascript/dashboard/components/SidemenuIcon.vue +++ b/app/javascript/dashboard/components/SidemenuIcon.vue @@ -1,5 +1,7 @@ +