Skip to content

Commit 4b87908

Browse files
authored
[Dashboards]- Addition of tabs (#14756)
1 parent ef988aa commit 4b87908

File tree

6 files changed

+159
-38
lines changed

6 files changed

+159
-38
lines changed

packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/dashboard-actions/components/SaveDashboardSingleRecordAction.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ export const SaveDashboardSingleRecordAction = () => {
1717
const { setIsDashboardInEditMode } =
1818
useSetIsDashboardInEditMode(pageLayoutId);
1919

20-
const handleClick = () => {
21-
savePageLayout();
20+
const handleClick = async () => {
21+
await savePageLayout();
2222
setIsDashboardInEditMode(false);
2323
};
2424

packages/twenty-front/src/modules/page-layout/components/PageLayoutRendererContent.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { PageLayoutGridLayout } from '@/page-layout/components/PageLayoutGridLayout';
2+
import { useCreatePageLayoutTab } from '@/page-layout/hooks/useCreatePageLayoutTab';
23
import { useCurrentPageLayout } from '@/page-layout/hooks/useCurrentPageLayout';
4+
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
35
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
46
import { TabList } from '@/ui/layout/tab-list/components/TabList';
57
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
8+
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
69
import styled from '@emotion/styled';
710
import { isDefined } from 'twenty-shared/utils';
811

@@ -26,6 +29,14 @@ const StyledScrollWrapper = styled(ScrollWrapper)`
2629
export const PageLayoutRendererContent = () => {
2730
const { currentPageLayout } = useCurrentPageLayout();
2831

32+
const isPageLayoutInEditMode = useRecoilComponentValue(
33+
isPageLayoutInEditModeComponentState,
34+
);
35+
36+
const { createPageLayoutTab } = useCreatePageLayoutTab(currentPageLayout?.id);
37+
38+
const handleAddTab = isPageLayoutInEditMode ? createPageLayoutTab : undefined;
39+
2940
if (!isDefined(currentPageLayout)) {
3041
return null;
3142
}
@@ -38,6 +49,7 @@ export const PageLayoutRendererContent = () => {
3849
componentInstanceId={getTabListInstanceIdFromPageLayoutId(
3950
currentPageLayout.id,
4051
)}
52+
onAddTab={handleAddTab}
4153
/>
4254
<StyledScrollWrapper
4355
componentInstanceId={`scroll-wrapper-page-layout-${currentPageLayout.id}`}

packages/twenty-front/src/modules/page-layout/hooks/__tests__/useCreatePageLayoutTab.test.tsx

Lines changed: 116 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
22
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
3+
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
34
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
5+
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
46
import { act, renderHook } from '@testing-library/react';
7+
import { useSetRecoilState } from 'recoil';
8+
import { PageLayoutType } from '~/generated/graphql';
59
import { useCreatePageLayoutTab } from '../useCreatePageLayoutTab';
610
import {
7-
PageLayoutTestWrapper,
811
PAGE_LAYOUT_TEST_INSTANCE_ID,
12+
PageLayoutTestWrapper,
913
} from './PageLayoutTestWrapper';
1014

1115
jest.mock('uuid', () => ({
@@ -20,44 +24,49 @@ describe('useCreatePageLayoutTab', () => {
2024
it('should create a new tab with default title', () => {
2125
const uuidModule = require('uuid');
2226
uuidModule.v4.mockReturnValue('mock-uuid');
27+
2328
const { result } = renderHook(
2429
() => ({
2530
createTab: useCreatePageLayoutTab(PAGE_LAYOUT_TEST_INSTANCE_ID),
31+
pageLayoutDraft: useRecoilComponentValue(
32+
pageLayoutDraftComponentState,
33+
PAGE_LAYOUT_TEST_INSTANCE_ID,
34+
),
2635
pageLayoutCurrentLayouts: useRecoilComponentValue(
2736
pageLayoutCurrentLayoutsComponentState,
2837
PAGE_LAYOUT_TEST_INSTANCE_ID,
2938
),
30-
pageLayoutDraft: useRecoilComponentValue(
31-
pageLayoutDraftComponentState,
32-
PAGE_LAYOUT_TEST_INSTANCE_ID,
39+
activeTabId: useSetRecoilState(
40+
activeTabIdComponentState.atomFamily({
41+
instanceId: `${PAGE_LAYOUT_TEST_INSTANCE_ID}-tab-list`,
42+
}),
3343
),
3444
}),
3545
{
3646
wrapper: PageLayoutTestWrapper,
3747
},
3848
);
3949

40-
let newTabId: string;
4150
act(() => {
42-
newTabId = result.current.createTab.createPageLayoutTab();
51+
result.current.createTab.createPageLayoutTab();
4352
});
4453

45-
expect(result.current.pageLayoutDraft.tabs[0].id).toBe('tab-mock-uuid');
54+
expect(result.current.pageLayoutDraft.tabs).toHaveLength(1);
55+
expect(result.current.pageLayoutDraft.tabs[0].id).toBe('mock-uuid');
4656
expect(result.current.pageLayoutDraft.tabs[0].title).toBe('Tab 1');
4757
expect(result.current.pageLayoutDraft.tabs[0].position).toBe(0);
4858
expect(result.current.pageLayoutDraft.tabs[0].widgets).toEqual([]);
4959

50-
expect(result.current.pageLayoutCurrentLayouts['tab-mock-uuid']).toEqual({
60+
expect(result.current.pageLayoutCurrentLayouts['mock-uuid']).toEqual({
5161
desktop: [],
5262
mobile: [],
5363
});
54-
55-
expect(newTabId!).toBe('tab-mock-uuid');
5664
});
5765

5866
it('should create a new tab with custom title', () => {
5967
const uuidModule = require('uuid');
6068
uuidModule.v4.mockReturnValue('mock-uuid');
69+
6170
const { result } = renderHook(
6271
() => ({
6372
createTab: useCreatePageLayoutTab(PAGE_LAYOUT_TEST_INSTANCE_ID),
@@ -83,8 +92,9 @@ describe('useCreatePageLayoutTab', () => {
8392
it('should increment position for subsequent tabs', () => {
8493
const uuidModule = require('uuid');
8594
uuidModule.v4
86-
.mockReturnValueOnce('mock-uuid')
95+
.mockReturnValueOnce('mock-uuid-1')
8796
.mockReturnValueOnce('mock-uuid-2');
97+
8898
const { result } = renderHook(
8999
() => ({
90100
createTab: useCreatePageLayoutTab(PAGE_LAYOUT_TEST_INSTANCE_ID),
@@ -107,8 +117,10 @@ describe('useCreatePageLayoutTab', () => {
107117
});
108118

109119
expect(result.current.pageLayoutDraft.tabs).toHaveLength(2);
120+
expect(result.current.pageLayoutDraft.tabs[0].id).toBe('mock-uuid-1');
110121
expect(result.current.pageLayoutDraft.tabs[0].position).toBe(0);
111122
expect(result.current.pageLayoutDraft.tabs[0].title).toBe('Tab 1');
123+
expect(result.current.pageLayoutDraft.tabs[1].id).toBe('mock-uuid-2');
112124
expect(result.current.pageLayoutDraft.tabs[1].position).toBe(1);
113125
expect(result.current.pageLayoutDraft.tabs[1].title).toBe('Tab 2');
114126
});
@@ -118,6 +130,7 @@ describe('useCreatePageLayoutTab', () => {
118130
uuidModule.v4
119131
.mockReturnValueOnce('mock-uuid-1')
120132
.mockReturnValueOnce('mock-uuid-2');
133+
121134
const { result } = renderHook(
122135
() => ({
123136
createTab: useCreatePageLayoutTab(PAGE_LAYOUT_TEST_INSTANCE_ID),
@@ -131,24 +144,109 @@ describe('useCreatePageLayoutTab', () => {
131144
},
132145
);
133146

134-
let tabId1: string = '';
135147
act(() => {
136-
tabId1 = result.current.createTab.createPageLayoutTab();
148+
result.current.createTab.createPageLayoutTab();
137149
});
138150

139-
let tabId2: string = '';
140151
act(() => {
141-
tabId2 = result.current.createTab.createPageLayoutTab();
152+
result.current.createTab.createPageLayoutTab();
142153
});
143154

144-
expect(result.current.pageLayoutCurrentLayouts[tabId1]).toEqual({
155+
const tabIds = Object.keys(result.current.pageLayoutCurrentLayouts);
156+
expect(tabIds).toHaveLength(2);
157+
expect(tabIds).toContain('mock-uuid-1');
158+
expect(tabIds).toContain('mock-uuid-2');
159+
160+
expect(result.current.pageLayoutCurrentLayouts['mock-uuid-1']).toEqual({
145161
desktop: [],
146162
mobile: [],
147163
});
148-
expect(result.current.pageLayoutCurrentLayouts[tabId2]).toEqual({
164+
expect(result.current.pageLayoutCurrentLayouts['mock-uuid-2']).toEqual({
149165
desktop: [],
150166
mobile: [],
151167
});
152-
expect(tabId1).not.toBe(tabId2);
168+
expect(tabIds[0]).not.toBe(tabIds[1]);
169+
});
170+
171+
it('should set newly created tab as active', () => {
172+
const uuidModule = require('uuid');
173+
uuidModule.v4.mockReturnValue('mock-uuid');
174+
175+
const { result } = renderHook(
176+
() => {
177+
const getActiveTabId = useRecoilComponentValue(
178+
activeTabIdComponentState,
179+
`${PAGE_LAYOUT_TEST_INSTANCE_ID}-tab-list`,
180+
);
181+
return {
182+
createTab: useCreatePageLayoutTab(PAGE_LAYOUT_TEST_INSTANCE_ID),
183+
activeTabId: getActiveTabId,
184+
};
185+
},
186+
{
187+
wrapper: PageLayoutTestWrapper,
188+
},
189+
);
190+
191+
expect(result.current.activeTabId).toBeNull();
192+
193+
act(() => {
194+
result.current.createTab.createPageLayoutTab();
195+
});
196+
197+
expect(result.current.activeTabId).toBe('mock-uuid');
198+
});
199+
200+
it('should handle creating tab when draft already has tabs', () => {
201+
const uuidModule = require('uuid');
202+
uuidModule.v4.mockReturnValue('mock-uuid-new');
203+
204+
const { result } = renderHook(
205+
() => {
206+
const setPageLayoutDraft = useSetRecoilComponentState(
207+
pageLayoutDraftComponentState,
208+
PAGE_LAYOUT_TEST_INSTANCE_ID,
209+
);
210+
const pageLayoutDraft = useRecoilComponentValue(
211+
pageLayoutDraftComponentState,
212+
PAGE_LAYOUT_TEST_INSTANCE_ID,
213+
);
214+
const createTab = useCreatePageLayoutTab(PAGE_LAYOUT_TEST_INSTANCE_ID);
215+
return { setPageLayoutDraft, pageLayoutDraft, createTab };
216+
},
217+
{
218+
wrapper: PageLayoutTestWrapper,
219+
},
220+
);
221+
222+
act(() => {
223+
result.current.setPageLayoutDraft({
224+
id: 'test-layout',
225+
name: 'Test Layout',
226+
type: PageLayoutType.DASHBOARD,
227+
objectMetadataId: null,
228+
tabs: [
229+
{
230+
id: 'existing-tab',
231+
title: 'Existing Tab',
232+
position: 0,
233+
pageLayoutId: 'test-layout',
234+
widgets: [],
235+
createdAt: new Date().toISOString(),
236+
updatedAt: new Date().toISOString(),
237+
deletedAt: null,
238+
},
239+
],
240+
});
241+
});
242+
243+
act(() => {
244+
result.current.createTab.createPageLayoutTab();
245+
});
246+
247+
expect(result.current.pageLayoutDraft.tabs).toHaveLength(2);
248+
expect(result.current.pageLayoutDraft.tabs[1].id).toBe('mock-uuid-new');
249+
expect(result.current.pageLayoutDraft.tabs[1].position).toBe(1);
250+
expect(result.current.pageLayoutDraft.tabs[1].title).toBe('Tab 2');
153251
});
154252
});

packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutTab.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
2+
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
3+
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
24
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
35
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
6+
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
47
import { useRecoilCallback } from 'recoil';
58
import { v4 as uuidv4 } from 'uuid';
69
import { pageLayoutCurrentLayoutsComponentState } from '../states/pageLayoutCurrentLayoutsComponentState';
@@ -24,20 +27,26 @@ export const useCreatePageLayoutTab = (pageLayoutIdFromProps?: string) => {
2427
pageLayoutId,
2528
);
2629

30+
const tabListInstanceId = getTabListInstanceIdFromPageLayoutId(pageLayoutId);
31+
const setActiveTabId = useSetRecoilComponentState(
32+
activeTabIdComponentState,
33+
tabListInstanceId,
34+
);
35+
2736
const createPageLayoutTab = useRecoilCallback(
2837
({ snapshot, set }) =>
29-
(title?: string): string => {
38+
(title?: string): void => {
3039
const pageLayoutDraft = snapshot
3140
.getLoadable(pageLayoutDraftState)
3241
.getValue();
3342

34-
const newTabId = `tab-${uuidv4()}`;
43+
const newTabId = uuidv4();
3544
const tabsLength = pageLayoutDraft.tabs.length;
3645
const newTab: PageLayoutTab = {
3746
id: newTabId,
3847
title: title || `Tab ${tabsLength + 1}`,
3948
position: tabsLength,
40-
pageLayoutId: '',
49+
pageLayoutId: pageLayoutId,
4150
widgets: [],
4251
createdAt: new Date().toISOString(),
4352
updatedAt: new Date().toISOString(),
@@ -55,9 +64,14 @@ export const useCreatePageLayoutTab = (pageLayoutIdFromProps?: string) => {
5564
getEmptyTabLayout(prev, newTabId),
5665
);
5766

58-
return newTabId;
67+
setActiveTabId(newTabId);
5968
},
60-
[pageLayoutCurrentLayoutsState, pageLayoutDraftState],
69+
[
70+
pageLayoutCurrentLayoutsState,
71+
pageLayoutDraftState,
72+
pageLayoutId,
73+
setActiveTabId,
74+
],
6175
);
6276

6377
return { createPageLayoutTab };

packages/twenty-front/src/modules/page-layout/utils/transformPageLayout.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ export const transformPageLayout = (
77
): PageLayout => {
88
return {
99
...pageLayout,
10-
tabs: (pageLayout.tabs ?? []).map(
11-
(tab): PageLayoutTab => ({
12-
...tab,
13-
widgets: tab.widgets ?? [],
14-
}),
15-
),
10+
tabs: (pageLayout.tabs ?? [])
11+
.toSorted((a, b) => a.position - b.position)
12+
.map(
13+
(tab): PageLayoutTab => ({
14+
...tab,
15+
widgets: tab.widgets ?? [],
16+
}),
17+
),
1618
};
1719
};

packages/twenty-front/src/modules/ui/layout/tab-list/components/TabList.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -218,12 +218,7 @@ export const TabList = ({
218218
{onAddTab && (
219219
<NodeDimension onDimensionChange={handleAddButtonWidthChange}>
220220
<StyledAddButton>
221-
<IconButton
222-
Icon={IconPlus}
223-
size="small"
224-
variant="tertiary"
225-
onClick={onAddTab}
226-
/>
221+
<IconButton Icon={IconPlus} size="small" variant="tertiary" />
227222
</StyledAddButton>
228223
</NodeDimension>
229224
)}
@@ -274,7 +269,7 @@ export const TabList = ({
274269
Icon={IconPlus}
275270
size="small"
276271
variant="tertiary"
277-
onClick={onAddTab}
272+
onClick={() => onAddTab()}
278273
/>
279274
</StyledAddButton>
280275
)}

0 commit comments

Comments
 (0)