diff --git a/app/javascript/dashboard/assets/scss/widgets/_base.scss b/app/javascript/dashboard/assets/scss/widgets/_base.scss index d5d2e227f..d35c1cfc9 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_base.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_base.scss @@ -31,7 +31,13 @@ hr { ul, ol, dl { - @apply mb-2 list-disc list-outside leading-[1.65]; + @apply list-disc list-outside leading-[1.65]; +} + +ul:not(.reset-base), +ol:not(.reset-base), +dl:not(.reset-base) { + @apply mb-0; } // Form elements diff --git a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss index 587b810ca..640e1395f 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss @@ -96,7 +96,7 @@ button { } // @TODDO - Remove after moving all buttons to woot-button - .icon+.button__content { + .icon + .button__content { @apply w-auto; } diff --git a/app/javascript/dashboard/components-next/dropdown-menu/DropdownPrimitives.story.vue b/app/javascript/dashboard/components-next/dropdown-menu/DropdownPrimitives.story.vue new file mode 100644 index 000000000..c0818283e --- /dev/null +++ b/app/javascript/dashboard/components-next/dropdown-menu/DropdownPrimitives.story.vue @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + {{ $t('SIDEBAR.SET_AUTO_OFFLINE.TEXT') }} + + + + + + + + + + + + + diff --git a/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownBody.vue b/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownBody.vue new file mode 100644 index 000000000..4fde89306 --- /dev/null +++ b/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownBody.vue @@ -0,0 +1,9 @@ + + + + + + + diff --git a/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownContainer.vue b/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownContainer.vue new file mode 100644 index 000000000..ee81e4b98 --- /dev/null +++ b/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownContainer.vue @@ -0,0 +1,29 @@ + + + + + + + + + + diff --git a/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownItem.vue b/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownItem.vue new file mode 100644 index 000000000..92be04fda --- /dev/null +++ b/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownItem.vue @@ -0,0 +1,55 @@ + + + + + + + + + + {{ label }} + + + + diff --git a/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownSection.vue b/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownSection.vue new file mode 100644 index 000000000..3879af0a7 --- /dev/null +++ b/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownSection.vue @@ -0,0 +1,22 @@ + + + + + + {{ title }} + + + + + + diff --git a/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownSeparator.vue b/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownSeparator.vue new file mode 100644 index 000000000..373aa4479 --- /dev/null +++ b/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownSeparator.vue @@ -0,0 +1,3 @@ + + + diff --git a/app/javascript/dashboard/components-next/dropdown-menu/base/index.js b/app/javascript/dashboard/components-next/dropdown-menu/base/index.js new file mode 100644 index 000000000..69a15a850 --- /dev/null +++ b/app/javascript/dashboard/components-next/dropdown-menu/base/index.js @@ -0,0 +1,13 @@ +import DropdownBody from './DropdownBody.vue'; +import DropdownContainer from './DropdownContainer.vue'; +import DropdownItem from './DropdownItem.vue'; +import DropdownSection from './DropdownSection.vue'; +import DropdownSeparator from './DropdownSeparator.vue'; + +export { + DropdownBody, + DropdownContainer, + DropdownItem, + DropdownSection, + DropdownSeparator, +}; diff --git a/app/javascript/dashboard/components-next/dropdown-menu/base/provider.js b/app/javascript/dashboard/components-next/dropdown-menu/base/provider.js new file mode 100644 index 000000000..67f2cbd0a --- /dev/null +++ b/app/javascript/dashboard/components-next/dropdown-menu/base/provider.js @@ -0,0 +1,19 @@ +import { inject, provide } from 'vue'; + +const DropdownControl = Symbol('DropdownControl'); + +export function useDropdownContext() { + const context = inject(DropdownControl, null); + + if (context === null) { + throw new Error( + `Component is missing a parent component.` + ); + } + + return context; +} + +export function provideDropdownContext(context) { + provide(DropdownControl, context); +} diff --git a/app/javascript/dashboard/components-next/sidebar/SidebarAccountSwitcher.vue b/app/javascript/dashboard/components-next/sidebar/SidebarAccountSwitcher.vue index 8e99977dd..03e7a5e53 100644 --- a/app/javascript/dashboard/components-next/sidebar/SidebarAccountSwitcher.vue +++ b/app/javascript/dashboard/components-next/sidebar/SidebarAccountSwitcher.vue @@ -1,24 +1,23 @@ - - - + + - {{ currentAccount.name }} - - - - - - - - {{ t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS') }} - - - + {{ currentAccount.name }} + + + + + + + + + { icon="i-lucide-check" class="text-n-teal-11 size-5" /> - - - - - {{ t('CREATE_ACCOUNT.NEW_ACCOUNT') }} - - - - - + + + + + + {{ t('CREATE_ACCOUNT.NEW_ACCOUNT') }} + + + + diff --git a/app/javascript/dashboard/components-next/sidebar/SidebarGroup.vue b/app/javascript/dashboard/components-next/sidebar/SidebarGroup.vue index f7bc2362d..43856e574 100644 --- a/app/javascript/dashboard/components-next/sidebar/SidebarGroup.vue +++ b/app/javascript/dashboard/components-next/sidebar/SidebarGroup.vue @@ -101,7 +101,7 @@ const toggleTrigger = () => { :permissions="resolvePermissions(to)" :feature-flag="resolveFeatureFlag(to)" as="li" - class="text-sm cursor-pointer select-none gap-1 grid" + class="grid gap-1 text-sm cursor-pointer select-none" > { { - if (showProfileMenu.value) { - emit('close'); - toggleProfileMenu(false); - } -}; const menuItems = computed(() => { return [ @@ -37,7 +34,6 @@ const menuItems = computed(() => { label: t('SIDEBAR_ITEMS.CONTACT_SUPPORT'), icon: 'i-lucide-life-buoy', click: () => { - closeMenu(); window.$chatwoot.toggle(); }, }, @@ -46,7 +42,6 @@ const menuItems = computed(() => { label: t('SIDEBAR_ITEMS.KEYBOARD_SHORTCUTS'), icon: 'i-lucide-keyboard', click: () => { - closeMenu(); emit('openKeyShortcutModal'); }, }, @@ -55,20 +50,26 @@ const menuItems = computed(() => { label: t('SIDEBAR_ITEMS.PROFILE_SETTINGS'), icon: 'i-lucide-user-pen', click: () => { - closeMenu(); router.push({ name: 'profile_settings_index' }); }, }, { show: true, label: t('SIDEBAR_ITEMS.APPEARANCE'), - icon: 'i-lucide-swatch-book', + icon: 'i-lucide-palette', click: () => { - closeMenu(); const ninja = document.querySelector('ninja-keys'); ninja.open({ parent: 'appearance_settings' }); }, }, + { + show: true, + label: t('SIDEBAR_ITEMS.DOCS'), + icon: 'i-lucide-book', + click: () => { + window.open('https://www.chatwoot.com/hc/user-guide/en', '_blank'); + }, + }, { show: currentUser.value.type === 'SuperAdmin', label: t('SIDEBAR_ITEMS.SUPER_ADMIN_CONSOLE'), @@ -79,7 +80,7 @@ const menuItems = computed(() => { { show: true, label: t('SIDEBAR_ITEMS.LOGOUT'), - icon: 'i-lucide-log-out', + icon: 'i-lucide-power', click: Auth.logout, }, ]; @@ -91,56 +92,40 @@ const allowedMenuItems = computed(() => { - - - - - - {{ currentUser.available_name }} - - - {{ currentUser.email }} - - - - - + + - - - - - - - {{ item.label }} - - - - - - + + + + {{ currentUser.available_name }} + + + {{ currentUser.email }} + + + + + + + + + + + + diff --git a/app/javascript/dashboard/components-next/sidebar/SidebarProfileMenuStatus.vue b/app/javascript/dashboard/components-next/sidebar/SidebarProfileMenuStatus.vue index 9642e4103..418319cc3 100644 --- a/app/javascript/dashboard/components-next/sidebar/SidebarProfileMenuStatus.vue +++ b/app/javascript/dashboard/components-next/sidebar/SidebarProfileMenuStatus.vue @@ -1,11 +1,18 @@ - - - {{ t('SIDEBAR.SET_AVAILABILITY_TITLE') }} - - - - - - {{ status.label }} - - - - - - - - - - - - - + + + + + {{ $t('SIDEBAR.SET_YOUR_AVAILABILITY') }} + + + + + + + + + {{ activeStatus.label }} + + + + + + + + + + {{ $t('SIDEBAR.SET_AUTO_OFFLINE.TEXT') }} + - - {{ t('SIDEBAR.SET_AUTO_OFFLINE.INFO_SHORT') }} - - - - - + + + + diff --git a/app/javascript/dashboard/components-next/sidebar/SidebarSubGroup.vue b/app/javascript/dashboard/components-next/sidebar/SidebarSubGroup.vue index 8c7f5c3a7..52886ebde 100644 --- a/app/javascript/dashboard/components-next/sidebar/SidebarSubGroup.vue +++ b/app/javascript/dashboard/components-next/sidebar/SidebarSubGroup.vue @@ -52,7 +52,7 @@ useEventListener(scrollableContainer, 'scroll', () => { :icon class="my-1" /> - + - + { it('title and sub title exist', () => { const headerComponent = accountSelector.findComponent(WootModalHeader); const title = headerComponent.find('[data-test-id="modal-header-title"]'); - expect(title.text()).toBe('Switch Account'); + expect(title.text()).toBe('Switch account'); const content = headerComponent.find( '[data-test-id="modal-header-content"]' ); diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json index 414291dfd..9b2c5fde6 100644 --- a/app/javascript/dashboard/i18n/locale/en/settings.json +++ b/app/javascript/dashboard/i18n/locale/en/settings.json @@ -177,14 +177,16 @@ }, "SIDEBAR_ITEMS": { "CHANGE_AVAILABILITY_STATUS": "Change", - "CHANGE_ACCOUNTS": "Switch Account", - "CONTACT_SUPPORT": "Contact Support", + "CHANGE_ACCOUNTS": "Switch account", + "SWITCH_WORKSPACE": "Switch workspace", + "CONTACT_SUPPORT": "Contact support", "SELECTOR_SUBTITLE": "Select an account from the following list", - "PROFILE_SETTINGS": "Profile Settings", - "KEYBOARD_SHORTCUTS": "Keyboard Shortcuts", - "APPEARANCE": "Change Appearance", - "SUPER_ADMIN_CONSOLE": "Super Admin Console", - "LOGOUT": "Logout" + "PROFILE_SETTINGS": "Profile settings", + "KEYBOARD_SHORTCUTS": "Keyboard shortcuts", + "APPEARANCE": "Change appearance", + "SUPER_ADMIN_CONSOLE": "SuperAdmin console", + "DOCS": "Read documentation", + "LOGOUT": "Log out" }, "APP_GLOBAL": { "TRIAL_MESSAGE": "days trial remaining.", @@ -279,6 +281,7 @@ "REPORTS_INBOX": "Inbox", "REPORTS_TEAM": "Team", "SET_AVAILABILITY_TITLE": "Set yourself as", + "SET_YOUR_AVAILABILITY": "Set your availability", "SLA": "SLA", "CUSTOM_ROLES": "Custom Roles", "BETA": "Beta", diff --git a/app/javascript/dashboard/store/modules/auth.js b/app/javascript/dashboard/store/modules/auth.js index e2c1aac75..695edc3cb 100644 --- a/app/javascript/dashboard/store/modules/auth.js +++ b/app/javascript/dashboard/store/modules/auth.js @@ -151,8 +151,15 @@ export const actions = { } }, - updateAvailability: async ({ commit, dispatch }, params) => { + updateAvailability: async ( + { commit, dispatch, getters: _getters }, + params + ) => { + const previousStatus = _getters.getCurrentUserAvailability; + try { + // optimisticly update current status + commit(types.SET_CURRENT_USER_AVAILABILITY, params.availability); const response = await authAPI.updateAvailability(params); const userData = response.data; const { id } = userData; @@ -162,16 +169,23 @@ export const actions = { availabilityStatus: params.availability, }); } catch (error) { - // Ignore error + // revert back to previous status if update fails + commit(types.SET_CURRENT_USER_AVAILABILITY, previousStatus); } }, - updateAutoOffline: async ({ commit }, { accountId, autoOffline }) => { + updateAutoOffline: async ( + { commit, getters: _getters }, + { accountId, autoOffline } + ) => { + const previousAutoOffline = _getters.getCurrentUserAutoOffline; + try { + commit(types.SET_CURRENT_USER_AUTO_OFFLINE, autoOffline); const response = await authAPI.updateAutoOffline(accountId, autoOffline); commit(types.SET_CURRENT_USER, response.data); } catch (error) { - // Ignore error + commit(types.SET_CURRENT_USER_AUTO_OFFLINE, previousAutoOffline); } }, @@ -212,6 +226,19 @@ export const mutations = { accounts, }; }, + [types.SET_CURRENT_USER_AUTO_OFFLINE](_state, autoOffline) { + const accounts = _state.currentUser.accounts.map(account => { + if (account.id === _state.currentUser.account_id) { + return { ...account, autoOffline: autoOffline }; + } + return account; + }); + + _state.currentUser = { + ..._state.currentUser, + accounts, + }; + }, [types.CLEAR_USER](_state) { _state.currentUser = initialState.currentUser; }, diff --git a/app/javascript/dashboard/store/modules/specs/auth/actions.spec.js b/app/javascript/dashboard/store/modules/specs/auth/actions.spec.js index f5206a8a6..3de56b1fe 100644 --- a/app/javascript/dashboard/store/modules/specs/auth/actions.spec.js +++ b/app/javascript/dashboard/store/modules/specs/auth/actions.spec.js @@ -1,7 +1,7 @@ import axios from 'axios'; import Cookies from 'js-cookie'; import { actions } from '../../auth'; -import * as types from '../../../mutation-types'; +import types from '../../../mutation-types'; import * as APIHelpers from '../../../utils/api'; import '../../../../routes'; @@ -25,7 +25,7 @@ describe('#actions', () => { await actions.validityCheck({ commit }); expect(APIHelpers.setUser).toHaveBeenCalledTimes(1); expect(commit.mock.calls).toEqual([ - [types.default.SET_CURRENT_USER, { id: 1, name: 'John' }], + [types.SET_CURRENT_USER, { id: 1, name: 'John' }], ]); }); it('sends correct actions if API is error', async () => { @@ -45,7 +45,7 @@ describe('#actions', () => { }); await actions.updateProfile({ commit }, { name: 'Pranav' }); expect(commit.mock.calls).toEqual([ - [types.default.SET_CURRENT_USER, { id: 1, name: 'John' }], + [types.SET_CURRENT_USER, { id: 1, name: 'John' }], ]); }); }); @@ -61,12 +61,13 @@ describe('#actions', () => { headers: { expiry: 581842904 }, }); await actions.updateAvailability( - { commit, dispatch }, + { commit, dispatch, getters: { getCurrentUserAvailability: 'online' } }, { availability: 'offline', account_id: 1 } ); expect(commit.mock.calls).toEqual([ + [types.SET_CURRENT_USER_AVAILABILITY, 'offline'], [ - types.default.SET_CURRENT_USER, + types.SET_CURRENT_USER, { id: 1, name: 'John', @@ -81,6 +82,18 @@ describe('#actions', () => { ], ]); }); + + it('sends correct actions if API is a failure', async () => { + axios.post.mockRejectedValue({ error: 'Authentication Failure' }); + await actions.updateAvailability( + { commit, dispatch, getters: { getCurrentUserAvailability: 'online' } }, + { availability: 'offline', account_id: 1 } + ); + expect(commit.mock.calls).toEqual([ + [types.SET_CURRENT_USER_AVAILABILITY, 'offline'], + [types.SET_CURRENT_USER_AVAILABILITY, 'online'], + ]); + }); }); describe('#updateAutoOffline', () => { @@ -99,12 +112,13 @@ describe('#actions', () => { headers: { expiry: 581842904 }, }); await actions.updateAutoOffline( - { commit, dispatch }, + { commit, dispatch, getters: { getCurrentUserAutoOffline: true } }, { autoOffline: false, accountId: 1 } ); expect(commit.mock.calls).toEqual([ + [types.SET_CURRENT_USER_AUTO_OFFLINE, false], [ - types.default.SET_CURRENT_USER, + types.SET_CURRENT_USER, { id: 1, name: 'John', @@ -113,6 +127,17 @@ describe('#actions', () => { ], ]); }); + it('sends correct actions if API is failure', async () => { + axios.post.mockRejectedValue({ error: 'Authentication Failure' }); + await actions.updateAutoOffline( + { commit, dispatch, getters: { getCurrentUserAutoOffline: true } }, + { autoOffline: false, accountId: 1 } + ); + expect(commit.mock.calls).toEqual([ + [types.SET_CURRENT_USER_AUTO_OFFLINE, false], + [types.SET_CURRENT_USER_AUTO_OFFLINE, true], + ]); + }); }); describe('#updateUISettings', () => { @@ -132,11 +157,11 @@ describe('#actions', () => { ); expect(commit.mock.calls).toEqual([ [ - types.default.SET_CURRENT_USER_UI_SETTINGS, + types.SET_CURRENT_USER_UI_SETTINGS, { uiSettings: { is_contact_sidebar_open: false } }, ], [ - types.default.SET_CURRENT_USER, + types.SET_CURRENT_USER, { id: 1, name: 'John', @@ -160,8 +185,8 @@ describe('#actions', () => { Cookies.get.mockImplementation(() => false); actions.setUser({ commit, dispatch }); expect(commit.mock.calls).toEqual([ - [types.default.CLEAR_USER], - [types.default.SET_CURRENT_USER_UI_FLAGS, { isFetching: false }], + [types.CLEAR_USER], + [types.SET_CURRENT_USER_UI_FLAGS, { isFetching: false }], ]); expect(dispatch).toHaveBeenCalledTimes(0); }); @@ -177,7 +202,7 @@ describe('#actions', () => { { 1: 'online' } ); expect(commit.mock.calls).toEqual([ - [types.default.SET_CURRENT_USER_AVAILABILITY, 'online'], + [types.SET_CURRENT_USER_AVAILABILITY, 'online'], ]); }); diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index 2a6bcacb8..7cc5223a0 100644 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -3,6 +3,7 @@ export default { CLEAR_USER: 'LOGOUT', SET_CURRENT_USER: 'SET_CURRENT_USER', SET_CURRENT_USER_AVAILABILITY: 'SET_CURRENT_USER_AVAILABILITY', + SET_CURRENT_USER_AUTO_OFFLINE: 'SET_CURRENT_USER_AUTO_OFFLINE', SET_CURRENT_USER_UI_SETTINGS: 'SET_CURRENT_USER_UI_SETTINGS', SET_CURRENT_USER_UI_FLAGS: 'SET_CURRENT_USER_UI_FLAGS',