mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-02 03:57:52 +00:00
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 <pranav@chatwoot.com> Co-authored-by: Pranav <pranavrajs@gmail.com>
This commit is contained in:
@@ -1,6 +1,38 @@
|
||||
import { defineComponent, h } from 'vue';
|
||||
import { createStore } from 'vuex';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { useAccount } from '../useAccount';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { mount } from '@vue/test-utils';
|
||||
|
||||
const store = createStore({
|
||||
modules: {
|
||||
auth: {
|
||||
namespaced: false,
|
||||
getters: {
|
||||
getCurrentAccountId: () => 1,
|
||||
getCurrentUser: () => ({
|
||||
accounts: [
|
||||
{ id: 1, name: 'Chatwoot', role: 'administrator' },
|
||||
{ id: 2, name: 'GitX', role: 'agent' },
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
accounts: {
|
||||
namespaced: true,
|
||||
getters: {
|
||||
getAccount: () => id => ({ id, name: 'Chatwoot' }),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const mountParams = {
|
||||
global: {
|
||||
plugins: [store],
|
||||
},
|
||||
};
|
||||
|
||||
vi.mock('vue-router');
|
||||
|
||||
@@ -8,31 +40,77 @@ describe('useAccount', () => {
|
||||
beforeEach(() => {
|
||||
useRoute.mockReturnValue({
|
||||
params: {
|
||||
accountId: 123,
|
||||
accountId: '123',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const createComponent = () =>
|
||||
defineComponent({
|
||||
setup() {
|
||||
return useAccount();
|
||||
},
|
||||
render() {
|
||||
return h('div'); // Dummy render to satisfy mount
|
||||
},
|
||||
});
|
||||
|
||||
it('returns accountId as a computed property', () => {
|
||||
const { accountId } = useAccount();
|
||||
expect(accountId.value).toBe(123);
|
||||
const wrapper = mount(createComponent(), mountParams);
|
||||
const { accountId } = wrapper.vm;
|
||||
expect(accountId).toBe(123);
|
||||
});
|
||||
|
||||
it('generates account-scoped URLs correctly', () => {
|
||||
const { accountScopedUrl } = useAccount();
|
||||
const wrapper = mount(createComponent(), mountParams);
|
||||
const { accountScopedUrl } = wrapper.vm;
|
||||
const result = accountScopedUrl('settings/inbox/new');
|
||||
expect(result).toBe('/app/accounts/123/settings/inbox/new');
|
||||
});
|
||||
|
||||
it('handles URLs with leading slash', () => {
|
||||
const { accountScopedUrl } = useAccount();
|
||||
const wrapper = mount(createComponent(), mountParams);
|
||||
const { accountScopedUrl } = wrapper.vm;
|
||||
const result = accountScopedUrl('users');
|
||||
expect(result).toBe('/app/accounts/123/users');
|
||||
expect(result).toBe('/app/accounts/123/users'); // Ensures no double slashes
|
||||
});
|
||||
|
||||
it('handles empty URL', () => {
|
||||
const { accountScopedUrl } = useAccount();
|
||||
const wrapper = mount(createComponent(), mountParams);
|
||||
const { accountScopedUrl } = wrapper.vm;
|
||||
const result = accountScopedUrl('');
|
||||
expect(result).toBe('/app/accounts/123/');
|
||||
});
|
||||
|
||||
it('returns current account based on accountId', () => {
|
||||
const wrapper = mount(createComponent(), mountParams);
|
||||
const { currentAccount } = wrapper.vm;
|
||||
expect(currentAccount).toEqual({ id: 123, name: 'Chatwoot' });
|
||||
});
|
||||
|
||||
it('returns an account-scoped route', () => {
|
||||
const wrapper = mount(createComponent(), mountParams);
|
||||
const { accountScopedRoute } = wrapper.vm;
|
||||
const result = accountScopedRoute('accountDetail', { userId: 456 });
|
||||
expect(result).toEqual({
|
||||
name: 'accountDetail',
|
||||
params: { accountId: 123, userId: 456 },
|
||||
});
|
||||
});
|
||||
|
||||
it('returns route with correct params', () => {
|
||||
const wrapper = mount(createComponent(), mountParams);
|
||||
const { route } = wrapper.vm;
|
||||
expect(route.params).toEqual({ accountId: '123' });
|
||||
});
|
||||
|
||||
it('handles non-numeric accountId gracefully', async () => {
|
||||
useRoute.mockReturnValueOnce({
|
||||
params: { accountId: 'abc' },
|
||||
});
|
||||
|
||||
const wrapper = mount(createComponent(), mountParams);
|
||||
const { accountId } = wrapper.vm;
|
||||
expect(accountId).toBeNaN(); // Handles invalid numeric conversion
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useMapGetter } from './store';
|
||||
|
||||
/**
|
||||
* Composable for account-related operations.
|
||||
@@ -11,11 +12,13 @@ export function useAccount() {
|
||||
* @type {import('vue').ComputedRef<number>}
|
||||
*/
|
||||
const route = useRoute();
|
||||
|
||||
const getAccountFn = useMapGetter('accounts/getAccount');
|
||||
const accountId = computed(() => {
|
||||
return Number(route.params.accountId);
|
||||
});
|
||||
|
||||
const currentAccount = computed(() => getAccountFn.value(accountId.value));
|
||||
|
||||
/**
|
||||
* Generates an account-scoped URL.
|
||||
* @param {string} url - The URL to be scoped to the account.
|
||||
@@ -25,8 +28,18 @@ export function useAccount() {
|
||||
return `/app/accounts/${accountId.value}/${url}`;
|
||||
};
|
||||
|
||||
const accountScopedRoute = (name, params) => {
|
||||
return {
|
||||
name,
|
||||
params: { accountId: accountId.value, ...params },
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
accountId,
|
||||
route,
|
||||
currentAccount,
|
||||
accountScopedUrl,
|
||||
accountScopedRoute,
|
||||
};
|
||||
}
|
||||
|
||||
29
app/javascript/dashboard/composables/usePolicy.js
Normal file
29
app/javascript/dashboard/composables/usePolicy.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import {
|
||||
getUserPermissions,
|
||||
hasPermissions,
|
||||
} from 'dashboard/helper/permissionsHelper';
|
||||
|
||||
export function usePolicy() {
|
||||
const user = useMapGetter('getCurrentUser');
|
||||
const isFeatureEnabled = useMapGetter('accounts/isFeatureEnabledonAccount');
|
||||
const { accountId } = useAccount();
|
||||
|
||||
const getUserPermissionsForAccount = () => {
|
||||
return getUserPermissions(user.value, accountId.value);
|
||||
};
|
||||
|
||||
const checkFeatureAllowed = featureFlag => {
|
||||
if (!featureFlag) return true;
|
||||
return isFeatureEnabled.value(accountId.value, featureFlag);
|
||||
};
|
||||
|
||||
const checkPermissions = requiredPermissions => {
|
||||
if (!requiredPermissions || !requiredPermissions.length) return true;
|
||||
const userPermissions = getUserPermissionsForAccount();
|
||||
return hasPermissions(requiredPermissions, userPermissions);
|
||||
};
|
||||
|
||||
return { checkFeatureAllowed, checkPermissions };
|
||||
}
|
||||
22
app/javascript/dashboard/composables/utils/useKbd.js
Normal file
22
app/javascript/dashboard/composables/utils/useKbd.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { computed } from 'vue';
|
||||
|
||||
export function useKbd(keys) {
|
||||
const keySymbols = {
|
||||
$mod: navigator.platform.includes('Mac') ? '⌘' : 'Ctrl',
|
||||
shift: '⇧',
|
||||
alt: '⌥',
|
||||
ctrl: 'Ctrl',
|
||||
cmd: '⌘',
|
||||
option: '⌥',
|
||||
enter: '↩',
|
||||
tab: '⇥',
|
||||
esc: '⎋',
|
||||
};
|
||||
|
||||
return computed(() => {
|
||||
return keys
|
||||
.map(key => keySymbols[key.toLowerCase()] || key)
|
||||
.join('')
|
||||
.toUpperCase();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user