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:
Shivam Mishra
2024-10-24 07:02:37 +05:30
committed by GitHub
parent 601a0f8a76
commit 6d3ecfe3c1
47 changed files with 2188 additions and 155 deletions

View File

@@ -11,10 +11,13 @@ describe('#actions', () => {
describe('#get', () => {
it('sends correct actions if API is success', async () => {
axios.get.mockResolvedValue({ data: customViewList });
await actions.get({ commit });
await actions.get({ commit }, 'conversation');
expect(commit.mock.calls).toEqual([
[types.default.SET_CUSTOM_VIEW_UI_FLAG, { isFetching: true }],
[types.default.SET_CUSTOM_VIEW, customViewList],
[
types.default.SET_CUSTOM_VIEW,
{ data: customViewList, filterType: 'conversation' },
],
[types.default.SET_CUSTOM_VIEW_UI_FLAG, { isFetching: false }],
]);
});
@@ -30,11 +33,15 @@ describe('#actions', () => {
describe('#create', () => {
it('sends correct actions if API is success', async () => {
axios.post.mockResolvedValue({ data: customViewList[0] });
await actions.create({ commit }, customViewList[0]);
const firstItem = customViewList[0];
axios.post.mockResolvedValue({ data: firstItem });
await actions.create({ commit }, firstItem);
expect(commit.mock.calls).toEqual([
[types.default.SET_CUSTOM_VIEW_UI_FLAG, { isCreating: true }],
[types.default.ADD_CUSTOM_VIEW, customViewList[0]],
[
types.default.ADD_CUSTOM_VIEW,
{ data: firstItem, filterType: 'conversation' },
],
[types.default.SET_CUSTOM_VIEW_UI_FLAG, { isCreating: false }],
]);
});
@@ -54,7 +61,7 @@ describe('#actions', () => {
await actions.delete({ commit }, { id: 1, filterType: 'contact' });
expect(commit.mock.calls).toEqual([
[types.default.SET_CUSTOM_VIEW_UI_FLAG, { isDeleting: true }],
[types.default.DELETE_CUSTOM_VIEW, 1],
[types.default.DELETE_CUSTOM_VIEW, { data: 1, filterType: 'contact' }],
[types.default.SET_CUSTOM_VIEW_UI_FLAG, { isDeleting: false }],
]);
});
@@ -70,15 +77,15 @@ describe('#actions', () => {
describe('#update', () => {
it('sends correct actions if API is success', async () => {
axios.patch.mockResolvedValue({ data: updateCustomViewList[0] });
await actions.update(
{ commit },
updateCustomViewList[0].id,
updateCustomViewList[0]
);
const item = updateCustomViewList[0];
axios.patch.mockResolvedValue({ data: item });
await actions.update({ commit }, item);
expect(commit.mock.calls).toEqual([
[types.default.SET_CUSTOM_VIEW_UI_FLAG, { isCreating: true }],
[types.default.UPDATE_CUSTOM_VIEW, updateCustomViewList[0]],
[
types.default.UPDATE_CUSTOM_VIEW,
{ data: item, filterType: 'conversation' },
],
[types.default.SET_CUSTOM_VIEW_UI_FLAG, { isCreating: false }],
]);
});

View File

@@ -1,3 +1,20 @@
export const contactViewList = [
{
name: 'Custom view 1',
filter_type: 1,
query: {
payload: [
{
attribute_key: 'name',
filter_operator: 'equal_to',
values: ['john doe'],
query_operator: null,
},
],
},
},
];
export const customViewList = [
{
name: 'Custom view',
@@ -21,14 +38,14 @@ export const customViewList = [
},
{
name: 'Custom view 1',
filter_type: 1,
filter_type: 0,
query: {
payload: [
{
attribute_key: 'name',
attribute_key: 'assignee_id',
filter_operator: 'equal_to',
values: ['john doe'],
query_operator: null,
values: [45],
query_operator: 'and',
},
],
},

View File

@@ -1,49 +1,9 @@
import { getters } from '../../customViews';
import { customViewList } from './fixtures';
import { contactViewList, customViewList } from './fixtures';
describe('#getters', () => {
it('getCustomViews', () => {
const state = { records: customViewList };
expect(getters.getCustomViews(state)).toEqual([
{
name: 'Custom view',
filter_type: 0,
query: {
payload: [
{
attribute_key: 'assignee_id',
filter_operator: 'equal_to',
values: [45],
query_operator: 'and',
},
{
attribute_key: 'inbox_id',
filter_operator: 'equal_to',
values: [144],
query_operator: 'and',
},
],
},
},
{
name: 'Custom view 1',
filter_type: 1,
query: {
payload: [
{
attribute_key: 'name',
filter_operator: 'equal_to',
values: ['john doe'],
query_operator: null,
},
],
},
},
]);
});
it('getCustomViewsByFilterType', () => {
const state = { records: customViewList };
const state = { contact: { records: contactViewList } };
expect(getters.getCustomViewsByFilterType(state)(1)).toEqual([
{
name: 'Custom view 1',

View File

@@ -4,34 +4,122 @@ import { customViewList, updateCustomViewList } from './fixtures';
describe('#mutations', () => {
describe('#SET_CUSTOM_VIEW', () => {
it('set custom view records', () => {
const state = { records: [] };
mutations[types.SET_CUSTOM_VIEW](state, customViewList);
expect(state.records).toEqual(customViewList);
it('[Conversation] set custom view records', () => {
const state = {
records: [],
conversation: { records: [] },
contact: { records: [] },
};
mutations[types.SET_CUSTOM_VIEW](state, {
data: customViewList,
filterType: 'conversation',
});
expect(state.conversation.records).toEqual(customViewList);
expect(state.contact.records).toEqual([]);
});
it('[Contact] set custom view records', () => {
const state = {
records: [],
conversation: { records: [] },
contact: { records: [] },
};
mutations[types.SET_CUSTOM_VIEW](state, {
data: customViewList,
filterType: 'contact',
});
expect(state.contact.records).toEqual(customViewList);
expect(state.conversation.records).toEqual([]);
});
});
describe('#ADD_CUSTOM_VIEW', () => {
it('push newly created custom views to the store', () => {
const state = { records: [customViewList] };
mutations[types.ADD_CUSTOM_VIEW](state, customViewList[0]);
expect(state.records).toEqual([customViewList, customViewList[0]]);
it('[Conversation] push newly created custom views to the store', () => {
const state = {
conversation: { records: [customViewList] },
contact: { records: [] },
};
mutations[types.ADD_CUSTOM_VIEW](state, {
data: customViewList[0],
filterType: 'conversation',
});
expect(state.conversation.records).toEqual([
customViewList,
customViewList[0],
]);
expect(state.contact.records).toEqual([]);
});
it('[Contact] push newly created custom views to the store', () => {
const state = {
conversation: { records: [] },
contact: { records: [customViewList] },
};
mutations[types.ADD_CUSTOM_VIEW](state, {
data: customViewList[0],
filterType: 'contact',
});
expect(state.contact.records).toEqual([
customViewList,
customViewList[0],
]);
expect(state.conversation.records).toEqual([]);
});
});
describe('#DELETE_CUSTOM_VIEW', () => {
it('delete custom view record', () => {
const state = { records: [customViewList[0]] };
mutations[types.DELETE_CUSTOM_VIEW](state, customViewList[0]);
expect(state.records).toEqual([customViewList[0]]);
it('[Conversation] delete custom view record', () => {
const state = {
conversation: { records: [customViewList[0]] },
contact: { records: [] },
};
mutations[types.DELETE_CUSTOM_VIEW](state, {
data: customViewList[0],
filterType: 'conversation',
});
expect(state.conversation.records).toEqual([customViewList[0]]);
expect(state.contact.records).toEqual([]);
});
it('[Contact] delete custom view record', () => {
const state = {
contact: { records: [customViewList[0]] },
conversation: { records: [] },
};
mutations[types.DELETE_CUSTOM_VIEW](state, {
data: customViewList[0],
filterType: 'contact',
});
expect(state.contact.records).toEqual([customViewList[0]]);
expect(state.conversation.records).toEqual([]);
});
});
describe('#UPDATE_CUSTOM_VIEW', () => {
it('update custom view record', () => {
const state = { records: [updateCustomViewList[0]] };
mutations[types.UPDATE_CUSTOM_VIEW](state, updateCustomViewList[0]);
expect(state.records).toEqual(updateCustomViewList);
it('[Conversation] update custom view record', () => {
const state = {
conversation: { records: [updateCustomViewList[0]] },
contact: { records: [] },
};
mutations[types.UPDATE_CUSTOM_VIEW](state, {
data: updateCustomViewList[0],
filterType: 'conversation',
});
expect(state.conversation.records).toEqual(updateCustomViewList);
expect(state.contact.records).toEqual([]);
});
it('[Contact] update custom view record', () => {
const state = {
contact: { records: [updateCustomViewList[0]] },
conversation: { records: [] },
};
mutations[types.UPDATE_CUSTOM_VIEW](state, {
data: updateCustomViewList[0],
filterType: 'contact',
});
expect(state.contact.records).toEqual(updateCustomViewList);
expect(state.conversation.records).toEqual([]);
});
});