From 894db2645a0636b26cd15e75eef0f7ac2077b4ef Mon Sep 17 00:00:00 2001 From: nitin <142569587+ehconitin@users.noreply.github.com> Date: Mon, 8 Sep 2025 17:50:43 +0530 Subject: [PATCH] [Page Layout] - Review Refactor (#14348) addressing the review on https://github.com/twentyhq/twenty/pull/14318#pullrequestreview-3191930203 --- .../CommandMenuPageLayoutGraphTypeSelect.tsx | 18 +- .../CommandMenuPageLayoutIframeConfig.tsx | 6 +- .../components/GraphWidgetRenderer.tsx | 75 +++++++ .../PageLayoutInitializationEffect.tsx | 2 - .../WidgetRenderer.tsx} | 11 +- .../SettingsPageLayoutTabsInstanceId.ts | 2 + .../useChangePageLayoutDragSelection.test.ts | 139 ++++++++++++ ...test.ts => useCreatePageLayoutTab.test.ts} | 24 +- ...t.ts => useCreatePageLayoutWidget.test.ts} | 74 +++++-- .../useDeletePageLayoutWidget.test.ts | 41 ++++ .../useEndPageLayoutDragSelection.test.ts | 208 ++++++++++++++++++ .../__tests__/usePageLayoutDraftState.test.ts | 7 +- .../usePageLayoutDragSelection.test.ts | 100 --------- .../usePageLayoutWidgetDelete.test.ts | 41 ---- .../useStartPageLayoutDragSelection.test.ts | 73 ++++++ .../hooks/useChangePageLayoutDragSelection.ts | 22 ++ .../hooks/useCreatePageLayoutIframeWidget.ts | 15 +- ...TabCreate.ts => useCreatePageLayoutTab.ts} | 6 +- ...Create.ts => useCreatePageLayoutWidget.ts} | 24 +- ...Delete.ts => useDeletePageLayoutWidget.ts} | 9 +- .../hooks/useEndPageLayoutDragSelection.ts | 43 ++++ .../hooks/usePageLayoutDraftState.ts | 1 - .../hooks/usePageLayoutDragSelection.ts | 61 ----- .../hooks/usePageLayoutSaveHandler.ts | 1 - .../hooks/useStartPageLayoutDragSelection.ts | 14 ++ ...Update.ts => useUpdatePageLayoutWidget.ts} | 6 +- .../settings/page-layout/mocks/mockWidgets.ts | 10 +- .../pageLayoutCurrentTabIdForCreation.ts | 8 - .../states/pageLayoutDraftState.ts | 1 - .../states/savedPageLayoutsState.ts | 1 - .../__tests__/convertLayoutsToWidgets.test.ts | 6 +- .../page-layout/utils/getDefaultWidgetData.ts | 37 ++-- .../page-layout/utils/graphRegistry.tsx | 71 ------ .../__stories__/Tablist.stories.tsx | 72 ++++++ .../utils/calculateVisibleTabCount.ts | 5 +- .../page-layout/SettingsPageLayoutEdit.tsx | 71 +++--- 36 files changed, 863 insertions(+), 442 deletions(-) create mode 100644 packages/twenty-front/src/modules/settings/page-layout/components/GraphWidgetRenderer.tsx rename packages/twenty-front/src/modules/settings/page-layout/{utils/widgetRegistry.tsx => components/WidgetRenderer.tsx} (71%) create mode 100644 packages/twenty-front/src/modules/settings/page-layout/constants/SettingsPageLayoutTabsInstanceId.ts create mode 100644 packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/useChangePageLayoutDragSelection.test.ts rename packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/{usePageLayoutTabCreate.test.ts => useCreatePageLayoutTab.test.ts} (83%) rename packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/{usePageLayoutWidgetCreate.test.ts => useCreatePageLayoutWidget.test.ts} (65%) create mode 100644 packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/useDeletePageLayoutWidget.test.ts create mode 100644 packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/useEndPageLayoutDragSelection.test.ts delete mode 100644 packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/usePageLayoutDragSelection.test.ts delete mode 100644 packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/usePageLayoutWidgetDelete.test.ts create mode 100644 packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/useStartPageLayoutDragSelection.test.ts create mode 100644 packages/twenty-front/src/modules/settings/page-layout/hooks/useChangePageLayoutDragSelection.ts rename packages/twenty-front/src/modules/settings/page-layout/hooks/{usePageLayoutTabCreate.ts => useCreatePageLayoutTab.ts} (90%) rename packages/twenty-front/src/modules/settings/page-layout/hooks/{usePageLayoutWidgetCreate.ts => useCreatePageLayoutWidget.ts} (79%) rename packages/twenty-front/src/modules/settings/page-layout/hooks/{usePageLayoutWidgetDelete.ts => useDeletePageLayoutWidget.ts} (84%) create mode 100644 packages/twenty-front/src/modules/settings/page-layout/hooks/useEndPageLayoutDragSelection.ts delete mode 100644 packages/twenty-front/src/modules/settings/page-layout/hooks/usePageLayoutDragSelection.ts create mode 100644 packages/twenty-front/src/modules/settings/page-layout/hooks/useStartPageLayoutDragSelection.ts rename packages/twenty-front/src/modules/settings/page-layout/hooks/{usePageLayoutWidgetUpdate.ts => useUpdatePageLayoutWidget.ts} (81%) delete mode 100644 packages/twenty-front/src/modules/settings/page-layout/states/pageLayoutCurrentTabIdForCreation.ts delete mode 100644 packages/twenty-front/src/modules/settings/page-layout/utils/graphRegistry.tsx diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect.tsx index ac9d099d614..022f20e6a75 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect.tsx @@ -1,7 +1,7 @@ import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; -import { usePageLayoutWidgetCreate } from '@/settings/page-layout/hooks/usePageLayoutWidgetCreate'; +import { useCreatePageLayoutWidget } from '@/settings/page-layout/hooks/useCreatePageLayoutWidget'; import { - GraphSubType, + GraphType, WidgetType, } from '@/settings/page-layout/mocks/mockWidgets'; import styled from '@emotion/styled'; @@ -30,22 +30,22 @@ const StyledSectionTitle = styled.div` const graphTypeOptions = [ { - type: GraphSubType.BAR, + type: GraphType.BAR, icon: IconChartBar, title: 'Bar Chart', }, { - type: GraphSubType.PIE, + type: GraphType.PIE, icon: IconChartPie, title: 'Pie Chart', }, { - type: GraphSubType.GAUGE, + type: GraphType.GAUGE, icon: IconGauge, title: 'Gauge', }, { - type: GraphSubType.NUMBER, + type: GraphType.NUMBER, icon: IconNumber, title: 'Number', }, @@ -53,10 +53,10 @@ const graphTypeOptions = [ export const CommandMenuPageLayoutGraphTypeSelect = () => { const { closeCommandMenu } = useCommandMenu(); - const { handleCreateWidget } = usePageLayoutWidgetCreate(); + const { createPageLayoutWidget } = useCreatePageLayoutWidget(); - const handleSelectGraphType = (graphType: GraphSubType) => { - handleCreateWidget(WidgetType.GRAPH, graphType); + const handleSelectGraphType = (graphType: GraphType) => { + createPageLayoutWidget(WidgetType.GRAPH, graphType); closeCommandMenu(); }; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeConfig.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeConfig.tsx index 783fd0333d8..cbe7263d64d 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeConfig.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeConfig.tsx @@ -1,7 +1,7 @@ import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput'; import { useCreatePageLayoutIframeWidget } from '@/settings/page-layout/hooks/useCreatePageLayoutIframeWidget'; -import { usePageLayoutWidgetUpdate } from '@/settings/page-layout/hooks/usePageLayoutWidgetUpdate'; +import { useUpdatePageLayoutWidget } from '@/settings/page-layout/hooks/useUpdatePageLayoutWidget'; import { pageLayoutDraftState } from '@/settings/page-layout/states/pageLayoutDraftState'; import { pageLayoutEditingWidgetIdState } from '@/settings/page-layout/states/pageLayoutEditingWidgetIdState'; import styled from '@emotion/styled'; @@ -34,7 +34,7 @@ const StyledButtonContainer = styled.div` export const CommandMenuPageLayoutIframeConfig = () => { const { closeCommandMenu } = useCommandMenu(); const { createPageLayoutIframeWidget } = useCreatePageLayoutIframeWidget(); - const { handleUpdateWidget } = usePageLayoutWidgetUpdate(); + const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(); const [pageLayoutEditingWidgetId, setPageLayoutEditingWidgetId] = useRecoilState(pageLayoutEditingWidgetIdState); const pageLayoutDraft = useRecoilValue(pageLayoutDraftState); @@ -77,7 +77,7 @@ export const CommandMenuPageLayoutIframeConfig = () => { } if (isEditMode && pageLayoutEditingWidgetId !== null) { - handleUpdateWidget(pageLayoutEditingWidgetId, { + updatePageLayoutWidget(pageLayoutEditingWidgetId, { title: title.trim(), configuration: { ...editingWidget?.configuration, diff --git a/packages/twenty-front/src/modules/settings/page-layout/components/GraphWidgetRenderer.tsx b/packages/twenty-front/src/modules/settings/page-layout/components/GraphWidgetRenderer.tsx new file mode 100644 index 00000000000..eaa2215958c --- /dev/null +++ b/packages/twenty-front/src/modules/settings/page-layout/components/GraphWidgetRenderer.tsx @@ -0,0 +1,75 @@ +import { GraphWidgetBarChart } from '@/dashboards/widgets/graph/components/GraphWidgetBarChart'; +import { GraphWidgetGaugeChart } from '@/dashboards/widgets/graph/components/GraphWidgetGaugeChart'; +import { GraphWidgetNumberChart } from '@/dashboards/widgets/graph/components/GraphWidgetNumberChart'; +import { GraphWidgetPieChart } from '@/dashboards/widgets/graph/components/GraphWidgetPieChart'; +import { GraphType } from '../mocks/mockWidgets'; +import { type PageLayoutWidget } from '../states/savedPageLayoutsState'; + +type GraphWidgetRendererProps = { + widget: PageLayoutWidget; +}; + +export const GraphWidgetRenderer = ({ widget }: GraphWidgetRendererProps) => { + const graphType = widget.configuration?.graphType; + + if (!graphType || typeof graphType !== 'string') { + return null; + } + + if (!Object.values(GraphType).includes(graphType as GraphType)) { + return null; + } + + switch (graphType as GraphType) { + case GraphType.NUMBER: + return ( + + ); + + case GraphType.GAUGE: + return ( + + ); + + case GraphType.PIE: + return ( + + ); + + case GraphType.BAR: + return ( + + ); + + default: + return null; + } +}; diff --git a/packages/twenty-front/src/modules/settings/page-layout/components/PageLayoutInitializationEffect.tsx b/packages/twenty-front/src/modules/settings/page-layout/components/PageLayoutInitializationEffect.tsx index 9b1b2003eda..e334a2ab084 100644 --- a/packages/twenty-front/src/modules/settings/page-layout/components/PageLayoutInitializationEffect.tsx +++ b/packages/twenty-front/src/modules/settings/page-layout/components/PageLayoutInitializationEffect.tsx @@ -42,7 +42,6 @@ export const PageLayoutInitializationEffect = ({ set(pageLayoutDraftState, { name: layout.name, type: layout.type, - workspaceId: layout.workspaceId, objectMetadataId: layout.objectMetadataId, tabs: layout.tabs, }); @@ -82,7 +81,6 @@ export const PageLayoutInitializationEffect = ({ set(pageLayoutDraftState, { name: '', type: PageLayoutType.DASHBOARD, - workspaceId: undefined, objectMetadataId: null, tabs: [defaultTab], }); diff --git a/packages/twenty-front/src/modules/settings/page-layout/utils/widgetRegistry.tsx b/packages/twenty-front/src/modules/settings/page-layout/components/WidgetRenderer.tsx similarity index 71% rename from packages/twenty-front/src/modules/settings/page-layout/utils/widgetRegistry.tsx rename to packages/twenty-front/src/modules/settings/page-layout/components/WidgetRenderer.tsx index 9adb4324e92..1eb0fedaa08 100644 --- a/packages/twenty-front/src/modules/settings/page-layout/utils/widgetRegistry.tsx +++ b/packages/twenty-front/src/modules/settings/page-layout/components/WidgetRenderer.tsx @@ -1,14 +1,17 @@ import { IframeWidget } from '@/dashboards/widgets/iframe/components/IframeWidget'; import { isString } from '@sniptt/guards'; -import { type ReactNode } from 'react'; import { WidgetType } from '../mocks/mockWidgets'; import { type PageLayoutWidget } from '../states/savedPageLayoutsState'; -import { renderGraphWidget } from './graphRegistry'; +import { GraphWidgetRenderer } from './GraphWidgetRenderer'; -export const renderWidget = (widget: PageLayoutWidget): ReactNode => { +type WidgetRendererProps = { + widget: PageLayoutWidget; +}; + +export const WidgetRenderer = ({ widget }: WidgetRendererProps) => { switch (widget.type) { case WidgetType.GRAPH: - return renderGraphWidget(widget); + return ; case WidgetType.IFRAME: { const url = widget.configuration?.url; diff --git a/packages/twenty-front/src/modules/settings/page-layout/constants/SettingsPageLayoutTabsInstanceId.ts b/packages/twenty-front/src/modules/settings/page-layout/constants/SettingsPageLayoutTabsInstanceId.ts new file mode 100644 index 00000000000..92044d62dee --- /dev/null +++ b/packages/twenty-front/src/modules/settings/page-layout/constants/SettingsPageLayoutTabsInstanceId.ts @@ -0,0 +1,2 @@ +export const SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID = + 'settings-page-layout-tabs'; diff --git a/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/useChangePageLayoutDragSelection.test.ts b/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/useChangePageLayoutDragSelection.test.ts new file mode 100644 index 00000000000..3914c73aed0 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/useChangePageLayoutDragSelection.test.ts @@ -0,0 +1,139 @@ +import { act, renderHook } from '@testing-library/react'; +import { type ReactNode } from 'react'; +import { RecoilRoot, useRecoilValue } from 'recoil'; +import { pageLayoutSelectedCellsState } from '../../states/pageLayoutSelectedCellsState'; +import { useChangePageLayoutDragSelection } from '../useChangePageLayoutDragSelection'; + +describe('useChangePageLayoutDragSelection', () => { + it('should add cell to selection when selected is true', () => { + const { result } = renderHook( + () => ({ + changeDragSelection: useChangePageLayoutDragSelection(), + selectedCells: useRecoilValue(pageLayoutSelectedCellsState), + }), + { + wrapper: ({ children }: { children: ReactNode }) => + RecoilRoot({ + initializeState: ({ set }) => { + set(pageLayoutSelectedCellsState, new Set(['cell-1'])); + }, + children, + }), + }, + ); + + expect(result.current.selectedCells.size).toBe(1); + expect(result.current.selectedCells.has('cell-2')).toBe(false); + + act(() => { + result.current.changeDragSelection.changePageLayoutDragSelection( + 'cell-2', + true, + ); + }); + + expect(result.current.selectedCells.size).toBe(2); + expect(result.current.selectedCells.has('cell-1')).toBe(true); + expect(result.current.selectedCells.has('cell-2')).toBe(true); + }); + + it('should remove cell from selection when selected is false', () => { + const { result } = renderHook( + () => ({ + changeDragSelection: useChangePageLayoutDragSelection(), + selectedCells: useRecoilValue(pageLayoutSelectedCellsState), + }), + { + wrapper: ({ children }: { children: ReactNode }) => + RecoilRoot({ + initializeState: ({ set }) => { + set(pageLayoutSelectedCellsState, new Set(['cell-1', 'cell-2'])); + }, + children, + }), + }, + ); + + expect(result.current.selectedCells.size).toBe(2); + expect(result.current.selectedCells.has('cell-2')).toBe(true); + + act(() => { + result.current.changeDragSelection.changePageLayoutDragSelection( + 'cell-2', + false, + ); + }); + + expect(result.current.selectedCells.size).toBe(1); + expect(result.current.selectedCells.has('cell-1')).toBe(true); + expect(result.current.selectedCells.has('cell-2')).toBe(false); + }); + + it('should handle adding same cell multiple times', () => { + const { result } = renderHook( + () => ({ + changeDragSelection: useChangePageLayoutDragSelection(), + selectedCells: useRecoilValue(pageLayoutSelectedCellsState), + }), + { + wrapper: RecoilRoot, + }, + ); + + act(() => { + result.current.changeDragSelection.changePageLayoutDragSelection( + 'cell-1', + true, + ); + }); + expect(result.current.selectedCells.size).toBe(1); + + act(() => { + result.current.changeDragSelection.changePageLayoutDragSelection( + 'cell-1', + true, + ); + }); + expect(result.current.selectedCells.size).toBe(1); + }); + + it('should handle removing non-existent cell', () => { + const { result } = renderHook( + () => ({ + changeDragSelection: useChangePageLayoutDragSelection(), + selectedCells: useRecoilValue(pageLayoutSelectedCellsState), + }), + { + wrapper: ({ children }: { children: ReactNode }) => + RecoilRoot({ + initializeState: ({ set }) => { + set(pageLayoutSelectedCellsState, new Set(['cell-1'])); + }, + children, + }), + }, + ); + + expect(result.current.selectedCells.size).toBe(1); + + act(() => { + result.current.changeDragSelection.changePageLayoutDragSelection( + 'cell-99', + false, + ); + }); + + expect(result.current.selectedCells.size).toBe(1); + expect(result.current.selectedCells.has('cell-1')).toBe(true); + }); + + it('should return a function', () => { + const { result } = renderHook(() => useChangePageLayoutDragSelection(), { + wrapper: RecoilRoot, + }); + + expect(typeof result.current.changePageLayoutDragSelection).toBe( + 'function', + ); + }); +}); diff --git a/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/usePageLayoutTabCreate.test.ts b/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/useCreatePageLayoutTab.test.ts similarity index 83% rename from packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/usePageLayoutTabCreate.test.ts rename to packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/useCreatePageLayoutTab.test.ts index de03664e2bd..7c938be7cc1 100644 --- a/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/usePageLayoutTabCreate.test.ts +++ b/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/useCreatePageLayoutTab.test.ts @@ -2,13 +2,13 @@ import { pageLayoutCurrentLayoutsState } from '@/settings/page-layout/states/pag import { pageLayoutDraftState } from '@/settings/page-layout/states/pageLayoutDraftState'; import { act, renderHook } from '@testing-library/react'; import { RecoilRoot, useRecoilValue } from 'recoil'; -import { usePageLayoutTabCreate } from '../usePageLayoutTabCreate'; +import { useCreatePageLayoutTab } from '../useCreatePageLayoutTab'; jest.mock('uuid', () => ({ v4: jest.fn(), })); -describe('usePageLayoutTabCreate', () => { +describe('useCreatePageLayoutTab', () => { beforeEach(() => { jest.clearAllMocks(); }); @@ -18,7 +18,7 @@ describe('usePageLayoutTabCreate', () => { uuidModule.v4.mockReturnValue('mock-uuid'); const { result } = renderHook( () => ({ - createTab: usePageLayoutTabCreate(), + createTab: useCreatePageLayoutTab(), pageLayoutCurrentLayouts: useRecoilValue(pageLayoutCurrentLayoutsState), pageLayoutDraft: useRecoilValue(pageLayoutDraftState), }), @@ -29,7 +29,7 @@ describe('usePageLayoutTabCreate', () => { let newTabId: string; act(() => { - newTabId = result.current.createTab.handleCreateTab(); + newTabId = result.current.createTab.createPageLayoutTab(); }); expect(result.current.pageLayoutDraft.tabs[0].id).toBe('tab-mock-uuid'); @@ -50,7 +50,7 @@ describe('usePageLayoutTabCreate', () => { uuidModule.v4.mockReturnValue('mock-uuid'); const { result } = renderHook( () => ({ - createTab: usePageLayoutTabCreate(), + createTab: useCreatePageLayoutTab(), pageLayoutDraft: useRecoilValue(pageLayoutDraftState), }), { @@ -59,7 +59,7 @@ describe('usePageLayoutTabCreate', () => { ); act(() => { - result.current.createTab.handleCreateTab('Custom Tab Name'); + result.current.createTab.createPageLayoutTab('Custom Tab Name'); }); expect(result.current.pageLayoutDraft.tabs[0].title).toBe( @@ -74,7 +74,7 @@ describe('usePageLayoutTabCreate', () => { .mockReturnValueOnce('mock-uuid-2'); const { result } = renderHook( () => ({ - createTab: usePageLayoutTabCreate(), + createTab: useCreatePageLayoutTab(), pageLayoutDraft: useRecoilValue(pageLayoutDraftState), }), { @@ -83,11 +83,11 @@ describe('usePageLayoutTabCreate', () => { ); act(() => { - result.current.createTab.handleCreateTab(); + result.current.createTab.createPageLayoutTab(); }); act(() => { - result.current.createTab.handleCreateTab(); + result.current.createTab.createPageLayoutTab(); }); expect(result.current.pageLayoutDraft.tabs).toHaveLength(2); @@ -104,7 +104,7 @@ describe('usePageLayoutTabCreate', () => { .mockReturnValueOnce('mock-uuid-2'); const { result } = renderHook( () => ({ - createTab: usePageLayoutTabCreate(), + createTab: useCreatePageLayoutTab(), pageLayoutCurrentLayouts: useRecoilValue(pageLayoutCurrentLayoutsState), }), { @@ -114,12 +114,12 @@ describe('usePageLayoutTabCreate', () => { let tabId1: string = ''; act(() => { - tabId1 = result.current.createTab.handleCreateTab(); + tabId1 = result.current.createTab.createPageLayoutTab(); }); let tabId2: string = ''; act(() => { - tabId2 = result.current.createTab.handleCreateTab(); + tabId2 = result.current.createTab.createPageLayoutTab(); }); expect(result.current.pageLayoutCurrentLayouts[tabId1]).toEqual({ diff --git a/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/usePageLayoutWidgetCreate.test.ts b/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/useCreatePageLayoutWidget.test.ts similarity index 65% rename from packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/usePageLayoutWidgetCreate.test.ts rename to packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/useCreatePageLayoutWidget.test.ts index 836c3e3c1e2..c1098518931 100644 --- a/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/usePageLayoutWidgetCreate.test.ts +++ b/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/useCreatePageLayoutWidget.test.ts @@ -1,20 +1,21 @@ +import { SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID } from '@/settings/page-layout/constants/SettingsPageLayoutTabsInstanceId'; import { - GraphSubType, + GraphType, WidgetType, } from '@/settings/page-layout/mocks/mockWidgets'; import { pageLayoutCurrentLayoutsState } from '@/settings/page-layout/states/pageLayoutCurrentLayoutsState'; -import { pageLayoutCurrentTabIdForCreationState } from '@/settings/page-layout/states/pageLayoutCurrentTabIdForCreation'; import { pageLayoutDraftState } from '@/settings/page-layout/states/pageLayoutDraftState'; import { PageLayoutType } from '@/settings/page-layout/states/savedPageLayoutsState'; +import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; import { act, renderHook } from '@testing-library/react'; import { RecoilRoot, useRecoilValue, useSetRecoilState } from 'recoil'; -import { usePageLayoutWidgetCreate } from '../usePageLayoutWidgetCreate'; +import { useCreatePageLayoutWidget } from '../useCreatePageLayoutWidget'; jest.mock('uuid', () => ({ v4: jest.fn(() => 'mock-uuid'), })); -describe('usePageLayoutWidgetCreate', () => { +describe('useCreatePageLayoutWidget', () => { beforeEach(() => { jest.clearAllMocks(); }); @@ -23,7 +24,9 @@ describe('usePageLayoutWidgetCreate', () => { const { result } = renderHook( () => { const setActiveTabId = useSetRecoilState( - pageLayoutCurrentTabIdForCreationState, + activeTabIdComponentState.atomFamily({ + instanceId: SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID, + }), ); const setPageLayoutDraft = useSetRecoilState(pageLayoutDraftState); const pageLayoutDraft = useRecoilValue(pageLayoutDraftState); @@ -31,7 +34,7 @@ describe('usePageLayoutWidgetCreate', () => { const pageLayoutCurrentLayouts = useRecoilValue( pageLayoutCurrentLayoutsState, ); - const createWidget = usePageLayoutWidgetCreate(); + const createWidget = useCreatePageLayoutWidget(); return { setActiveTabId, setPageLayoutDraft, @@ -49,7 +52,6 @@ describe('usePageLayoutWidgetCreate', () => { result.current.setPageLayoutDraft({ name: 'Test Layout', type: PageLayoutType.DASHBOARD, - workspaceId: undefined, objectMetadataId: null, tabs: [ { @@ -68,9 +70,9 @@ describe('usePageLayoutWidgetCreate', () => { }); act(() => { - result.current.createWidget.handleCreateWidget( + result.current.createWidget.createPageLayoutWidget( WidgetType.GRAPH, - GraphSubType.BAR, + GraphType.BAR, ); }); @@ -89,12 +91,22 @@ describe('usePageLayoutWidgetCreate', () => { () => { const setPageLayoutDraft = useSetRecoilState(pageLayoutDraftState); const setActiveTabId = useSetRecoilState( - pageLayoutCurrentTabIdForCreationState, + activeTabIdComponentState.atomFamily({ + instanceId: SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID, + }), ); - const createWidget = usePageLayoutWidgetCreate(); + const pageLayoutDraft = useRecoilValue(pageLayoutDraftState); + const allWidgets = pageLayoutDraft.tabs.flatMap((tab) => tab.widgets); + const pageLayoutCurrentLayouts = useRecoilValue( + pageLayoutCurrentLayoutsState, + ); + const createWidget = useCreatePageLayoutWidget(); return { setPageLayoutDraft, setActiveTabId, + pageLayoutDraft, + allWidgets, + pageLayoutCurrentLayouts, createWidget, }; }, @@ -107,7 +119,6 @@ describe('usePageLayoutWidgetCreate', () => { result.current.setPageLayoutDraft({ name: 'Test Layout', type: PageLayoutType.DASHBOARD, - workspaceId: undefined, objectMetadataId: null, tabs: [ { @@ -126,24 +137,41 @@ describe('usePageLayoutWidgetCreate', () => { }); const graphTypes = [ - GraphSubType.NUMBER, - GraphSubType.GAUGE, - GraphSubType.PIE, - GraphSubType.BAR, + GraphType.NUMBER, + GraphType.GAUGE, + GraphType.PIE, + GraphType.BAR, ]; graphTypes.forEach((graphType) => { act(() => { - result.current.createWidget.handleCreateWidget( + result.current.createWidget.createPageLayoutWidget( WidgetType.GRAPH, graphType, ); }); }); - expect(typeof result.current.createWidget.handleCreateWidget).toBe( - 'function', - ); + expect(result.current.allWidgets).toHaveLength(4); + + graphTypes.forEach((graphType, index) => { + const widget = result.current.allWidgets[index]; + expect(widget.type).toBe(WidgetType.GRAPH); + expect(widget.pageLayoutTabId).toBe('tab-1'); + expect(widget.configuration?.graphType).toBe(graphType); + expect(widget.id).toBe('widget-mock-uuid'); + expect(widget.data).toBeDefined(); + }); + + expect(result.current.pageLayoutCurrentLayouts['tab-1']).toBeDefined(); + expect( + result.current.pageLayoutCurrentLayouts['tab-1'].desktop, + ).toHaveLength(4); + expect( + result.current.pageLayoutCurrentLayouts['tab-1'].mobile, + ).toHaveLength(4); + + expect(result.current.pageLayoutDraft.tabs[0].widgets).toHaveLength(4); }); it('should not create widget when activeTabId is null', () => { @@ -154,7 +182,7 @@ describe('usePageLayoutWidgetCreate', () => { const pageLayoutCurrentLayouts = useRecoilValue( pageLayoutCurrentLayoutsState, ); - const createWidget = usePageLayoutWidgetCreate(); + const createWidget = useCreatePageLayoutWidget(); return { allWidgets, pageLayoutCurrentLayouts, createWidget }; }, { @@ -163,9 +191,9 @@ describe('usePageLayoutWidgetCreate', () => { ); act(() => { - result.current.createWidget.handleCreateWidget( + result.current.createWidget.createPageLayoutWidget( WidgetType.GRAPH, - GraphSubType.BAR, + GraphType.BAR, ); }); diff --git a/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/useDeletePageLayoutWidget.test.ts b/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/useDeletePageLayoutWidget.test.ts new file mode 100644 index 00000000000..0873d5f1124 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/useDeletePageLayoutWidget.test.ts @@ -0,0 +1,41 @@ +import { act, renderHook } from '@testing-library/react'; +import { RecoilRoot } from 'recoil'; +import { useDeletePageLayoutWidget } from '../useDeletePageLayoutWidget'; + +describe('useDeletePageLayoutWidget', () => { + it('should remove widget from all states', () => { + const { result } = renderHook(() => useDeletePageLayoutWidget(), { + wrapper: RecoilRoot, + }); + + act(() => { + result.current.deletePageLayoutWidget('widget-1'); + }); + + expect(typeof result.current.deletePageLayoutWidget).toBe('function'); + }); + + it('should handle removing non-existent widget', () => { + const { result } = renderHook(() => useDeletePageLayoutWidget(), { + wrapper: RecoilRoot, + }); + + act(() => { + result.current.deletePageLayoutWidget('non-existent-widget'); + }); + + expect(typeof result.current.deletePageLayoutWidget).toBe('function'); + }); + + it('should handle empty layouts', () => { + const { result } = renderHook(() => useDeletePageLayoutWidget(), { + wrapper: RecoilRoot, + }); + + act(() => { + result.current.deletePageLayoutWidget('any-widget'); + }); + + expect(typeof result.current.deletePageLayoutWidget).toBe('function'); + }); +}); diff --git a/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/useEndPageLayoutDragSelection.test.ts b/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/useEndPageLayoutDragSelection.test.ts new file mode 100644 index 00000000000..7d24cfd1216 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/useEndPageLayoutDragSelection.test.ts @@ -0,0 +1,208 @@ +import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu'; +import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { act, renderHook } from '@testing-library/react'; +import { type ReactNode } from 'react'; +import { RecoilRoot, useRecoilValue } from 'recoil'; +import { IconAppWindow } from 'twenty-ui/display'; +import { pageLayoutDraggedAreaState } from '../../states/pageLayoutDraggedAreaState'; +import { pageLayoutSelectedCellsState } from '../../states/pageLayoutSelectedCellsState'; +import { calculateGridBoundsFromSelectedCells } from '../../utils/calculateGridBoundsFromSelectedCells'; +import { useEndPageLayoutDragSelection } from '../useEndPageLayoutDragSelection'; + +jest.mock('@/command-menu/hooks/useNavigateCommandMenu'); +jest.mock('../../utils/calculateGridBoundsFromSelectedCells'); + +describe('useEndPageLayoutDragSelection', () => { + const mockNavigateCommandMenu = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + (useNavigateCommandMenu as jest.Mock).mockReturnValue({ + navigateCommandMenu: mockNavigateCommandMenu, + }); + }); + + it('should handle drag selection end with valid bounds', () => { + const mockBounds = { x: 0, y: 0, w: 2, h: 2 }; + (calculateGridBoundsFromSelectedCells as jest.Mock).mockReturnValue( + mockBounds, + ); + + const { result } = renderHook( + () => ({ + endDragSelection: useEndPageLayoutDragSelection(), + selectedCells: useRecoilValue(pageLayoutSelectedCellsState), + draggedArea: useRecoilValue(pageLayoutDraggedAreaState), + }), + { + wrapper: ({ children }: { children: ReactNode }) => + RecoilRoot({ + initializeState: ({ set }) => { + set( + pageLayoutSelectedCellsState, + new Set(['0-0', '0-1', '1-0', '1-1']), + ); + set(pageLayoutDraggedAreaState, null); + }, + children, + }), + }, + ); + + expect(result.current.selectedCells.size).toBe(4); + expect(result.current.draggedArea).toBeNull(); + + act(() => { + result.current.endDragSelection.endPageLayoutDragSelection(); + }); + + expect(calculateGridBoundsFromSelectedCells).toHaveBeenCalledWith([ + '0-0', + '0-1', + '1-0', + '1-1', + ]); + + expect(result.current.draggedArea).toEqual(mockBounds); + expect(result.current.selectedCells.size).toBe(0); + + expect(mockNavigateCommandMenu).toHaveBeenCalledWith({ + page: CommandMenuPages.PageLayoutWidgetTypeSelect, + pageTitle: 'Add Widget', + pageIcon: IconAppWindow, + resetNavigationStack: true, + }); + }); + + it('should not navigate when no cells are selected', () => { + (calculateGridBoundsFromSelectedCells as jest.Mock).mockReturnValue(null); + + const { result } = renderHook( + () => ({ + endDragSelection: useEndPageLayoutDragSelection(), + selectedCells: useRecoilValue(pageLayoutSelectedCellsState), + draggedArea: useRecoilValue(pageLayoutDraggedAreaState), + }), + { + wrapper: ({ children }: { children: ReactNode }) => + RecoilRoot({ + initializeState: ({ set }) => { + set(pageLayoutSelectedCellsState, new Set()); + set(pageLayoutDraggedAreaState, null); + }, + children, + }), + }, + ); + + act(() => { + result.current.endDragSelection.endPageLayoutDragSelection(); + }); + + expect(calculateGridBoundsFromSelectedCells).not.toHaveBeenCalled(); + expect(mockNavigateCommandMenu).not.toHaveBeenCalled(); + expect(result.current.draggedArea).toBeNull(); + expect(result.current.selectedCells.size).toBe(0); + }); + + it('should not navigate when bounds calculation returns null', () => { + (calculateGridBoundsFromSelectedCells as jest.Mock).mockReturnValue(null); + + const { result } = renderHook( + () => ({ + endDragSelection: useEndPageLayoutDragSelection(), + selectedCells: useRecoilValue(pageLayoutSelectedCellsState), + draggedArea: useRecoilValue(pageLayoutDraggedAreaState), + }), + { + wrapper: ({ children }: { children: ReactNode }) => + RecoilRoot({ + initializeState: ({ set }) => { + set(pageLayoutSelectedCellsState, new Set(['invalid-cell'])); + set(pageLayoutDraggedAreaState, null); + }, + children, + }), + }, + ); + + act(() => { + result.current.endDragSelection.endPageLayoutDragSelection(); + }); + + expect(calculateGridBoundsFromSelectedCells).toHaveBeenCalledWith([ + 'invalid-cell', + ]); + expect(mockNavigateCommandMenu).not.toHaveBeenCalled(); + expect(result.current.draggedArea).toBeNull(); + expect(result.current.selectedCells.size).toBe(1); + }); + + it('should return a function', () => { + const { result } = renderHook(() => useEndPageLayoutDragSelection(), { + wrapper: RecoilRoot, + }); + + expect(typeof result.current.endPageLayoutDragSelection).toBe('function'); + }); + + it('should navigate to widget selection when bounds are valid', () => { + const mockBounds = { x: 0, y: 0, w: 2, h: 2 }; + (calculateGridBoundsFromSelectedCells as jest.Mock).mockReturnValue( + mockBounds, + ); + + const { result } = renderHook( + () => ({ + endDragSelection: useEndPageLayoutDragSelection(), + selectedCells: useRecoilValue(pageLayoutSelectedCellsState), + }), + { + wrapper: ({ children }: { children: ReactNode }) => + RecoilRoot({ + initializeState: ({ set }) => { + set(pageLayoutSelectedCellsState, new Set(['0-0'])); + }, + children, + }), + }, + ); + + act(() => { + result.current.endDragSelection.endPageLayoutDragSelection(); + }); + + expect(mockNavigateCommandMenu).toHaveBeenCalled(); + }); + + it('should clear selected cells after successful navigation', () => { + const mockBounds = { x: 0, y: 0, w: 1, h: 1 }; + (calculateGridBoundsFromSelectedCells as jest.Mock).mockReturnValue( + mockBounds, + ); + + const { result } = renderHook( + () => ({ + endDragSelection: useEndPageLayoutDragSelection(), + selectedCells: useRecoilValue(pageLayoutSelectedCellsState), + }), + { + wrapper: ({ children }: { children: ReactNode }) => + RecoilRoot({ + initializeState: ({ set }) => { + set(pageLayoutSelectedCellsState, new Set(['0-0'])); + }, + children, + }), + }, + ); + + expect(result.current.selectedCells.size).toBe(1); + + act(() => { + result.current.endDragSelection.endPageLayoutDragSelection(); + }); + + expect(result.current.selectedCells.size).toBe(0); + }); +}); diff --git a/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/usePageLayoutDraftState.test.ts b/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/usePageLayoutDraftState.test.ts index dd60729f8b4..3e9326c82a0 100644 --- a/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/usePageLayoutDraftState.test.ts +++ b/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/usePageLayoutDraftState.test.ts @@ -1,5 +1,5 @@ import { - GraphSubType, + GraphType, WidgetType, } from '@/settings/page-layout/mocks/mockWidgets'; import { PageLayoutType } from '@/settings/page-layout/states/savedPageLayoutsState'; @@ -26,7 +26,6 @@ describe('usePageLayoutDraftState', () => { result.current.setPageLayoutDraft({ name: ' ', type: PageLayoutType.DASHBOARD, - workspaceId: undefined, objectMetadataId: null, tabs: [], }); @@ -45,7 +44,6 @@ describe('usePageLayoutDraftState', () => { result.current.setPageLayoutDraft({ name: 'Updated Name', type: PageLayoutType.DASHBOARD, - workspaceId: undefined, objectMetadataId: null, tabs: [], }); @@ -65,7 +63,6 @@ describe('usePageLayoutDraftState', () => { result.current.setPageLayoutDraft({ name: 'Test Layout', type: PageLayoutType.DASHBOARD, - workspaceId: undefined, objectMetadataId: null, tabs: [ { @@ -83,7 +80,7 @@ describe('usePageLayoutDraftState', () => { title: 'New Widget', type: WidgetType.GRAPH, gridPosition: { row: 2, column: 2, rowSpan: 2, columnSpan: 2 }, - configuration: { graphType: GraphSubType.BAR }, + configuration: { graphType: GraphType.BAR }, data: {}, objectMetadataId: null, createdAt: new Date().toISOString(), diff --git a/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/usePageLayoutDragSelection.test.ts b/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/usePageLayoutDragSelection.test.ts deleted file mode 100644 index 31de4c80847..00000000000 --- a/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/usePageLayoutDragSelection.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { act, renderHook } from '@testing-library/react'; -import { RecoilRoot } from 'recoil'; -import { usePageLayoutDragSelection } from '../usePageLayoutDragSelection'; - -describe('usePageLayoutDragSelection', () => { - it('should initialize with empty selected cells', () => { - const { result } = renderHook(() => usePageLayoutDragSelection(), { - wrapper: RecoilRoot, - }); - - expect(result.current.pageLayoutSelectedCells).toEqual(new Set()); - }); - - it('should clear selected cells on drag start', () => { - const { result } = renderHook(() => usePageLayoutDragSelection(), { - wrapper: RecoilRoot, - }); - - act(() => { - result.current.handleDragSelectionChange('cell-1', true); - result.current.handleDragSelectionChange('cell-2', true); - }); - - expect(result.current.pageLayoutSelectedCells.size).toBe(2); - - act(() => { - result.current.handleDragSelectionStart(); - }); - - expect(result.current.pageLayoutSelectedCells).toEqual(new Set()); - }); - - it('should add and remove cells during drag selection', () => { - const { result } = renderHook(() => usePageLayoutDragSelection(), { - wrapper: RecoilRoot, - }); - - act(() => { - result.current.handleDragSelectionChange('cell-1', true); - }); - - expect(result.current.pageLayoutSelectedCells.has('cell-1')).toBe(true); - - act(() => { - result.current.handleDragSelectionChange('cell-2', true); - }); - - expect(result.current.pageLayoutSelectedCells.size).toBe(2); - - act(() => { - result.current.handleDragSelectionChange('cell-1', false); - }); - - expect(result.current.pageLayoutSelectedCells.has('cell-1')).toBe(false); - expect(result.current.pageLayoutSelectedCells.size).toBe(1); - }); - - it('should handle drag selection end with selected cells', () => { - const { result } = renderHook(() => usePageLayoutDragSelection(), { - wrapper: RecoilRoot, - }); - - act(() => { - result.current.handleDragSelectionChange('0-0', true); - result.current.handleDragSelectionChange('1-0', true); - result.current.handleDragSelectionChange('0-1', true); - result.current.handleDragSelectionChange('1-1', true); - }); - - expect(result.current.pageLayoutSelectedCells.size).toBe(4); - - act(() => { - result.current.handleDragSelectionEnd(); - }); - - expect(result.current.pageLayoutSelectedCells).toEqual(new Set()); - }); - - it('should handle drag selection end with no selected cells', () => { - const { result } = renderHook(() => usePageLayoutDragSelection(), { - wrapper: RecoilRoot, - }); - - act(() => { - result.current.handleDragSelectionEnd(); - }); - - expect(result.current.pageLayoutSelectedCells).toEqual(new Set()); - }); - - it('should provide all required handler functions', () => { - const { result } = renderHook(() => usePageLayoutDragSelection(), { - wrapper: RecoilRoot, - }); - - expect(typeof result.current.handleDragSelectionStart).toBe('function'); - expect(typeof result.current.handleDragSelectionChange).toBe('function'); - expect(typeof result.current.handleDragSelectionEnd).toBe('function'); - }); -}); diff --git a/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/usePageLayoutWidgetDelete.test.ts b/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/usePageLayoutWidgetDelete.test.ts deleted file mode 100644 index 53bcfc4d6d9..00000000000 --- a/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/usePageLayoutWidgetDelete.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { act, renderHook } from '@testing-library/react'; -import { RecoilRoot } from 'recoil'; -import { usePageLayoutWidgetDelete } from '../usePageLayoutWidgetDelete'; - -describe('usePageLayoutWidgetDelete', () => { - it('should remove widget from all states', () => { - const { result } = renderHook(() => usePageLayoutWidgetDelete(), { - wrapper: RecoilRoot, - }); - - act(() => { - result.current.handleRemoveWidget('widget-1'); - }); - - expect(typeof result.current.handleRemoveWidget).toBe('function'); - }); - - it('should handle removing non-existent widget', () => { - const { result } = renderHook(() => usePageLayoutWidgetDelete(), { - wrapper: RecoilRoot, - }); - - act(() => { - result.current.handleRemoveWidget('non-existent-widget'); - }); - - expect(typeof result.current.handleRemoveWidget).toBe('function'); - }); - - it('should handle empty layouts', () => { - const { result } = renderHook(() => usePageLayoutWidgetDelete(), { - wrapper: RecoilRoot, - }); - - act(() => { - result.current.handleRemoveWidget('any-widget'); - }); - - expect(typeof result.current.handleRemoveWidget).toBe('function'); - }); -}); diff --git a/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/useStartPageLayoutDragSelection.test.ts b/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/useStartPageLayoutDragSelection.test.ts new file mode 100644 index 00000000000..296345a460a --- /dev/null +++ b/packages/twenty-front/src/modules/settings/page-layout/hooks/__tests__/useStartPageLayoutDragSelection.test.ts @@ -0,0 +1,73 @@ +import { act, renderHook } from '@testing-library/react'; +import { type ReactNode } from 'react'; +import { RecoilRoot, useRecoilValue } from 'recoil'; +import { pageLayoutSelectedCellsState } from '../../states/pageLayoutSelectedCellsState'; +import { useStartPageLayoutDragSelection } from '../useStartPageLayoutDragSelection'; + +describe('useStartPageLayoutDragSelection', () => { + it('should clear selected cells when starting drag selection', () => { + const { result } = renderHook( + () => ({ + startDragSelection: useStartPageLayoutDragSelection(), + selectedCells: useRecoilValue(pageLayoutSelectedCellsState), + }), + { + wrapper: ({ children }: { children: ReactNode }) => + RecoilRoot({ + initializeState: ({ set }) => { + set(pageLayoutSelectedCellsState, new Set(['cell-1', 'cell-2'])); + }, + children, + }), + }, + ); + + expect(result.current.selectedCells.size).toBe(2); + expect(result.current.selectedCells.has('cell-1')).toBe(true); + expect(result.current.selectedCells.has('cell-2')).toBe(true); + + act(() => { + result.current.startDragSelection.startPageLayoutDragSelection(); + }); + + expect(result.current.selectedCells.size).toBe(0); + }); + + it('should return a function', () => { + const { result } = renderHook(() => useStartPageLayoutDragSelection(), { + wrapper: RecoilRoot, + }); + + expect(typeof result.current.startPageLayoutDragSelection).toBe('function'); + }); + + it('should handle multiple calls correctly', () => { + const { result } = renderHook( + () => ({ + startDragSelection: useStartPageLayoutDragSelection(), + selectedCells: useRecoilValue(pageLayoutSelectedCellsState), + }), + { + wrapper: ({ children }: { children: ReactNode }) => + RecoilRoot({ + initializeState: ({ set }) => { + set(pageLayoutSelectedCellsState, new Set(['cell-1'])); + }, + children, + }), + }, + ); + + expect(result.current.selectedCells.size).toBe(1); + + act(() => { + result.current.startDragSelection.startPageLayoutDragSelection(); + }); + expect(result.current.selectedCells.size).toBe(0); + + act(() => { + result.current.startDragSelection.startPageLayoutDragSelection(); + }); + expect(result.current.selectedCells.size).toBe(0); + }); +}); diff --git a/packages/twenty-front/src/modules/settings/page-layout/hooks/useChangePageLayoutDragSelection.ts b/packages/twenty-front/src/modules/settings/page-layout/hooks/useChangePageLayoutDragSelection.ts new file mode 100644 index 00000000000..07d58859d36 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/page-layout/hooks/useChangePageLayoutDragSelection.ts @@ -0,0 +1,22 @@ +import { useRecoilCallback } from 'recoil'; +import { pageLayoutSelectedCellsState } from '../states/pageLayoutSelectedCellsState'; + +export const useChangePageLayoutDragSelection = () => { + const changePageLayoutDragSelection = useRecoilCallback( + ({ set }) => + (cellId: string, selected: boolean) => { + set(pageLayoutSelectedCellsState, (prev) => { + const newSet = new Set(prev); + if (selected) { + newSet.add(cellId); + } else { + newSet.delete(cellId); + } + return newSet; + }); + }, + [], + ); + + return { changePageLayoutDragSelection }; +}; diff --git a/packages/twenty-front/src/modules/settings/page-layout/hooks/useCreatePageLayoutIframeWidget.ts b/packages/twenty-front/src/modules/settings/page-layout/hooks/useCreatePageLayoutIframeWidget.ts index 05f51655b94..3ff4adccdaa 100644 --- a/packages/twenty-front/src/modules/settings/page-layout/hooks/useCreatePageLayoutIframeWidget.ts +++ b/packages/twenty-front/src/modules/settings/page-layout/hooks/useCreatePageLayoutIframeWidget.ts @@ -1,8 +1,10 @@ +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; import { useRecoilCallback } from 'recoil'; import { v4 as uuidv4 } from 'uuid'; +import { SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID } from '../constants/SettingsPageLayoutTabsInstanceId'; import { WidgetType } from '../mocks/mockWidgets'; import { pageLayoutCurrentLayoutsState } from '../states/pageLayoutCurrentLayoutsState'; -import { pageLayoutCurrentTabIdForCreationState } from '../states/pageLayoutCurrentTabIdForCreation'; import { pageLayoutDraftState } from '../states/pageLayoutDraftState'; import { pageLayoutDraggedAreaState } from '../states/pageLayoutDraggedAreaState'; import { type PageLayoutWidget } from '../states/savedPageLayoutsState'; @@ -11,6 +13,11 @@ import { createUpdatedTabLayouts } from '../utils/createUpdatedTabLayouts'; import { getDefaultWidgetPosition } from '../utils/getDefaultWidgetPosition'; export const useCreatePageLayoutIframeWidget = () => { + const activeTabId = useRecoilComponentValue( + activeTabIdComponentState, + SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID, + ); + const createPageLayoutIframeWidget = useRecoilCallback( ({ snapshot, set }) => (title: string, url: string) => { @@ -21,10 +28,6 @@ export const useCreatePageLayoutIframeWidget = () => { .getLoadable(pageLayoutDraggedAreaState) .getValue(); - const activeTabId = snapshot - .getLoadable(pageLayoutCurrentTabIdForCreationState) - .getValue(); - if (!activeTabId) { return; } @@ -78,7 +81,7 @@ export const useCreatePageLayoutIframeWidget = () => { set(pageLayoutDraggedAreaState, null); }, - [], + [activeTabId], ); return { createPageLayoutIframeWidget }; diff --git a/packages/twenty-front/src/modules/settings/page-layout/hooks/usePageLayoutTabCreate.ts b/packages/twenty-front/src/modules/settings/page-layout/hooks/useCreatePageLayoutTab.ts similarity index 90% rename from packages/twenty-front/src/modules/settings/page-layout/hooks/usePageLayoutTabCreate.ts rename to packages/twenty-front/src/modules/settings/page-layout/hooks/useCreatePageLayoutTab.ts index 03f5eb3aef8..e930b42f7b2 100644 --- a/packages/twenty-front/src/modules/settings/page-layout/hooks/usePageLayoutTabCreate.ts +++ b/packages/twenty-front/src/modules/settings/page-layout/hooks/useCreatePageLayoutTab.ts @@ -5,8 +5,8 @@ import { pageLayoutDraftState } from '../states/pageLayoutDraftState'; import { type PageLayoutTab } from '../states/savedPageLayoutsState'; import { createEmptyTabLayout } from '../utils/createEmptyTabLayout'; -export const usePageLayoutTabCreate = () => { - const handleCreateTab = useRecoilCallback( +export const useCreatePageLayoutTab = () => { + const createPageLayoutTab = useRecoilCallback( ({ snapshot, set }) => (title?: string): string => { const pageLayoutDraft = snapshot @@ -41,5 +41,5 @@ export const usePageLayoutTabCreate = () => { [], ); - return { handleCreateTab }; + return { createPageLayoutTab }; }; diff --git a/packages/twenty-front/src/modules/settings/page-layout/hooks/usePageLayoutWidgetCreate.ts b/packages/twenty-front/src/modules/settings/page-layout/hooks/useCreatePageLayoutWidget.ts similarity index 79% rename from packages/twenty-front/src/modules/settings/page-layout/hooks/usePageLayoutWidgetCreate.ts rename to packages/twenty-front/src/modules/settings/page-layout/hooks/useCreatePageLayoutWidget.ts index e9582f159bf..7bf4d188531 100644 --- a/packages/twenty-front/src/modules/settings/page-layout/hooks/usePageLayoutWidgetCreate.ts +++ b/packages/twenty-front/src/modules/settings/page-layout/hooks/useCreatePageLayoutWidget.ts @@ -1,8 +1,10 @@ +import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useRecoilCallback } from 'recoil'; import { v4 as uuidv4 } from 'uuid'; -import { type GraphSubType, type WidgetType } from '../mocks/mockWidgets'; +import { SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID } from '../constants/SettingsPageLayoutTabsInstanceId'; +import { type GraphType, type WidgetType } from '../mocks/mockWidgets'; import { pageLayoutCurrentLayoutsState } from '../states/pageLayoutCurrentLayoutsState'; -import { pageLayoutCurrentTabIdForCreationState } from '../states/pageLayoutCurrentTabIdForCreation'; import { pageLayoutDraftState } from '../states/pageLayoutDraftState'; import { pageLayoutDraggedAreaState } from '../states/pageLayoutDraggedAreaState'; import { type PageLayoutWidget } from '../states/savedPageLayoutsState'; @@ -15,10 +17,15 @@ import { } from '../utils/getDefaultWidgetData'; import { getDefaultWidgetPosition } from '../utils/getDefaultWidgetPosition'; -export const usePageLayoutWidgetCreate = () => { - const handleCreateWidget = useRecoilCallback( +export const useCreatePageLayoutWidget = () => { + const activeTabId = useRecoilComponentValue( + activeTabIdComponentState, + SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID, + ); + + const createPageLayoutWidget = useRecoilCallback( ({ snapshot, set }) => - (widgetType: WidgetType, graphType: GraphSubType) => { + (widgetType: WidgetType, graphType: GraphType) => { const widgetData = getDefaultWidgetData(graphType); const pageLayoutDraft = snapshot @@ -30,9 +37,6 @@ export const usePageLayoutWidgetCreate = () => { const pageLayoutDraggedArea = snapshot .getLoadable(pageLayoutDraggedAreaState) .getValue(); - const activeTabId = snapshot - .getLoadable(pageLayoutCurrentTabIdForCreationState) - .getValue(); if (!activeTabId) { return; @@ -95,8 +99,8 @@ export const usePageLayoutWidgetCreate = () => { set(pageLayoutDraggedAreaState, null); }, - [], + [activeTabId], ); - return { handleCreateWidget }; + return { createPageLayoutWidget }; }; diff --git a/packages/twenty-front/src/modules/settings/page-layout/hooks/usePageLayoutWidgetDelete.ts b/packages/twenty-front/src/modules/settings/page-layout/hooks/useDeletePageLayoutWidget.ts similarity index 84% rename from packages/twenty-front/src/modules/settings/page-layout/hooks/usePageLayoutWidgetDelete.ts rename to packages/twenty-front/src/modules/settings/page-layout/hooks/useDeletePageLayoutWidget.ts index faf0b622111..1365f88a7b2 100644 --- a/packages/twenty-front/src/modules/settings/page-layout/hooks/usePageLayoutWidgetDelete.ts +++ b/packages/twenty-front/src/modules/settings/page-layout/hooks/useDeletePageLayoutWidget.ts @@ -1,11 +1,12 @@ import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; import { pageLayoutCurrentLayoutsState } from '../states/pageLayoutCurrentLayoutsState'; import { pageLayoutDraftState } from '../states/pageLayoutDraftState'; import { removeWidgetFromTab } from '../utils/removeWidgetFromTab'; import { removeWidgetLayoutFromTab } from '../utils/removeWidgetLayoutFromTab'; -export const usePageLayoutWidgetDelete = () => { - const handleRemoveWidget = useRecoilCallback( +export const useDeletePageLayoutWidget = () => { + const deletePageLayoutWidget = useRecoilCallback( ({ snapshot, set }) => (widgetId: string) => { const pageLayoutDraft = snapshot @@ -20,7 +21,7 @@ export const usePageLayoutWidgetDelete = () => { ); const tabId = tabWithWidget?.id; - if (tabId !== undefined) { + if (isDefined(tabId)) { const updatedLayouts = removeWidgetLayoutFromTab( allTabLayouts, tabId, @@ -37,5 +38,5 @@ export const usePageLayoutWidgetDelete = () => { [], ); - return { handleRemoveWidget }; + return { deletePageLayoutWidget }; }; diff --git a/packages/twenty-front/src/modules/settings/page-layout/hooks/useEndPageLayoutDragSelection.ts b/packages/twenty-front/src/modules/settings/page-layout/hooks/useEndPageLayoutDragSelection.ts new file mode 100644 index 00000000000..5c579f24ec0 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/page-layout/hooks/useEndPageLayoutDragSelection.ts @@ -0,0 +1,43 @@ +import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu'; +import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; +import { IconAppWindow } from 'twenty-ui/display'; +import { pageLayoutDraggedAreaState } from '../states/pageLayoutDraggedAreaState'; +import { pageLayoutSelectedCellsState } from '../states/pageLayoutSelectedCellsState'; +import { calculateGridBoundsFromSelectedCells } from '../utils/calculateGridBoundsFromSelectedCells'; + +export const useEndPageLayoutDragSelection = () => { + const { navigateCommandMenu } = useNavigateCommandMenu(); + + const endPageLayoutDragSelection = useRecoilCallback( + ({ snapshot, set }) => + () => { + const pageLayoutSelectedCells = snapshot + .getLoadable(pageLayoutSelectedCellsState) + .getValue(); + + if (pageLayoutSelectedCells.size > 0) { + const draggedBounds = calculateGridBoundsFromSelectedCells( + Array.from(pageLayoutSelectedCells), + ); + + if (isDefined(draggedBounds)) { + set(pageLayoutDraggedAreaState, draggedBounds); + + navigateCommandMenu({ + page: CommandMenuPages.PageLayoutWidgetTypeSelect, + pageTitle: 'Add Widget', + pageIcon: IconAppWindow, + resetNavigationStack: true, + }); + + set(pageLayoutSelectedCellsState, new Set()); + } + } + }, + [navigateCommandMenu], + ); + + return { endPageLayoutDragSelection }; +}; diff --git a/packages/twenty-front/src/modules/settings/page-layout/hooks/usePageLayoutDraftState.ts b/packages/twenty-front/src/modules/settings/page-layout/hooks/usePageLayoutDraftState.ts index 45394025041..41c82a8a2c3 100644 --- a/packages/twenty-front/src/modules/settings/page-layout/hooks/usePageLayoutDraftState.ts +++ b/packages/twenty-front/src/modules/settings/page-layout/hooks/usePageLayoutDraftState.ts @@ -12,7 +12,6 @@ export const usePageLayoutDraftState = () => { ? !isDeeplyEqual(pageLayoutDraft, { name: pageLayoutPersisted.name, type: pageLayoutPersisted.type, - workspaceId: pageLayoutPersisted.workspaceId, objectMetadataId: pageLayoutPersisted.objectMetadataId, tabs: pageLayoutPersisted.tabs, }) diff --git a/packages/twenty-front/src/modules/settings/page-layout/hooks/usePageLayoutDragSelection.ts b/packages/twenty-front/src/modules/settings/page-layout/hooks/usePageLayoutDragSelection.ts deleted file mode 100644 index 25706d0a0ea..00000000000 --- a/packages/twenty-front/src/modules/settings/page-layout/hooks/usePageLayoutDragSelection.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu'; -import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; -import { useRecoilState, useSetRecoilState } from 'recoil'; -import { IconAppWindow } from 'twenty-ui/display'; -import { pageLayoutDraggedAreaState } from '../states/pageLayoutDraggedAreaState'; -import { pageLayoutSelectedCellsState } from '../states/pageLayoutSelectedCellsState'; -import { calculateGridBoundsFromSelectedCells } from '../utils/calculateGridBoundsFromSelectedCells'; - -export const usePageLayoutDragSelection = () => { - const [pageLayoutSelectedCells, setPageLayoutSelectedCells] = useRecoilState( - pageLayoutSelectedCellsState, - ); - const setPageLayoutDraggedArea = useSetRecoilState( - pageLayoutDraggedAreaState, - ); - - const { navigateCommandMenu } = useNavigateCommandMenu(); - const handleDragSelectionStart = () => { - setPageLayoutSelectedCells(new Set()); - }; - - const handleDragSelectionChange = (cellId: string, selected: boolean) => { - setPageLayoutSelectedCells((prev) => { - const newSet = new Set(prev); - if (selected) { - newSet.add(cellId); - } else { - newSet.delete(cellId); - } - return newSet; - }); - }; - - const handleDragSelectionEnd = () => { - if (pageLayoutSelectedCells.size > 0) { - const draggedBounds = calculateGridBoundsFromSelectedCells( - Array.from(pageLayoutSelectedCells), - ); - - if (draggedBounds !== null) { - setPageLayoutDraggedArea(draggedBounds); - - navigateCommandMenu({ - page: CommandMenuPages.PageLayoutWidgetTypeSelect, - pageTitle: 'Add Widget', - pageIcon: IconAppWindow, - resetNavigationStack: true, - }); - - setPageLayoutSelectedCells(new Set()); - } - } - }; - - return { - pageLayoutSelectedCells, - handleDragSelectionStart, - handleDragSelectionChange, - handleDragSelectionEnd, - }; -}; diff --git a/packages/twenty-front/src/modules/settings/page-layout/hooks/usePageLayoutSaveHandler.ts b/packages/twenty-front/src/modules/settings/page-layout/hooks/usePageLayoutSaveHandler.ts index 6f837443f7e..e15df7e10f1 100644 --- a/packages/twenty-front/src/modules/settings/page-layout/hooks/usePageLayoutSaveHandler.ts +++ b/packages/twenty-front/src/modules/settings/page-layout/hooks/usePageLayoutSaveHandler.ts @@ -42,7 +42,6 @@ export const usePageLayoutSaveHandler = () => { id: isEditMode ? id : uuidv4(), name: pageLayoutDraft.name, type: pageLayoutDraft.type, - workspaceId: pageLayoutDraft.workspaceId, objectMetadataId: pageLayoutDraft.objectMetadataId, tabs: updatedTabs, createdAt: isEditMode diff --git a/packages/twenty-front/src/modules/settings/page-layout/hooks/useStartPageLayoutDragSelection.ts b/packages/twenty-front/src/modules/settings/page-layout/hooks/useStartPageLayoutDragSelection.ts new file mode 100644 index 00000000000..f109425dcd1 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/page-layout/hooks/useStartPageLayoutDragSelection.ts @@ -0,0 +1,14 @@ +import { useRecoilCallback } from 'recoil'; +import { pageLayoutSelectedCellsState } from '../states/pageLayoutSelectedCellsState'; + +export const useStartPageLayoutDragSelection = () => { + const startPageLayoutDragSelection = useRecoilCallback( + ({ set }) => + () => { + set(pageLayoutSelectedCellsState, new Set()); + }, + [], + ); + + return { startPageLayoutDragSelection }; +}; diff --git a/packages/twenty-front/src/modules/settings/page-layout/hooks/usePageLayoutWidgetUpdate.ts b/packages/twenty-front/src/modules/settings/page-layout/hooks/useUpdatePageLayoutWidget.ts similarity index 81% rename from packages/twenty-front/src/modules/settings/page-layout/hooks/usePageLayoutWidgetUpdate.ts rename to packages/twenty-front/src/modules/settings/page-layout/hooks/useUpdatePageLayoutWidget.ts index ce1c1242d3a..772428991a8 100644 --- a/packages/twenty-front/src/modules/settings/page-layout/hooks/usePageLayoutWidgetUpdate.ts +++ b/packages/twenty-front/src/modules/settings/page-layout/hooks/useUpdatePageLayoutWidget.ts @@ -2,8 +2,8 @@ import { useRecoilCallback } from 'recoil'; import { type PageLayoutWidget } from '../states/savedPageLayoutsState'; import { pageLayoutDraftState } from '../states/pageLayoutDraftState'; -export const usePageLayoutWidgetUpdate = () => { - const handleUpdateWidget = useRecoilCallback( +export const useUpdatePageLayoutWidget = () => { + const updatePageLayoutWidget = useRecoilCallback( ({ set }) => (widgetId: string, updates: Partial) => { set(pageLayoutDraftState, (prev) => ({ @@ -19,5 +19,5 @@ export const usePageLayoutWidgetUpdate = () => { [], ); - return { handleUpdateWidget }; + return { updatePageLayoutWidget }; }; diff --git a/packages/twenty-front/src/modules/settings/page-layout/mocks/mockWidgets.ts b/packages/twenty-front/src/modules/settings/page-layout/mocks/mockWidgets.ts index 5a1ba46ed93..f931f28c317 100644 --- a/packages/twenty-front/src/modules/settings/page-layout/mocks/mockWidgets.ts +++ b/packages/twenty-front/src/modules/settings/page-layout/mocks/mockWidgets.ts @@ -8,7 +8,7 @@ export enum WidgetType { GRAPH = 'GRAPH', } -export enum GraphSubType { +export enum GraphType { NUMBER = 'NUMBER', GAUGE = 'GAUGE', PIE = 'PIE', @@ -29,7 +29,7 @@ export const mockPageLayoutWidgets: PageLayoutWidget[] = [ columnSpan: 3, }, configuration: { - graphType: GraphSubType.NUMBER, + graphType: GraphType.NUMBER, }, data: { value: '1,234', @@ -52,7 +52,7 @@ export const mockPageLayoutWidgets: PageLayoutWidget[] = [ columnSpan: 3, }, configuration: { - graphType: GraphSubType.GAUGE, + graphType: GraphType.GAUGE, }, data: { value: 0.5, @@ -77,7 +77,7 @@ export const mockPageLayoutWidgets: PageLayoutWidget[] = [ columnSpan: 6, }, configuration: { - graphType: GraphSubType.PIE, + graphType: GraphType.PIE, }, data: { items: [ @@ -130,7 +130,7 @@ export const mockPageLayoutWidgets: PageLayoutWidget[] = [ columnSpan: 4, }, configuration: { - graphType: GraphSubType.BAR, + graphType: GraphType.BAR, }, data: { items: [ diff --git a/packages/twenty-front/src/modules/settings/page-layout/states/pageLayoutCurrentTabIdForCreation.ts b/packages/twenty-front/src/modules/settings/page-layout/states/pageLayoutCurrentTabIdForCreation.ts deleted file mode 100644 index cf513be3e61..00000000000 --- a/packages/twenty-front/src/modules/settings/page-layout/states/pageLayoutCurrentTabIdForCreation.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createState } from 'twenty-ui/utilities'; - -export const pageLayoutCurrentTabIdForCreationState = createState< - string | null ->({ - key: 'pageLayoutCurrentTabIdForCreationState', - defaultValue: null, -}); diff --git a/packages/twenty-front/src/modules/settings/page-layout/states/pageLayoutDraftState.ts b/packages/twenty-front/src/modules/settings/page-layout/states/pageLayoutDraftState.ts index e7b20c5fa6a..e6c21e9a26e 100644 --- a/packages/twenty-front/src/modules/settings/page-layout/states/pageLayoutDraftState.ts +++ b/packages/twenty-front/src/modules/settings/page-layout/states/pageLayoutDraftState.ts @@ -11,7 +11,6 @@ export const pageLayoutDraftState = createState({ defaultValue: { name: '', type: PageLayoutType.DASHBOARD, - workspaceId: undefined, objectMetadataId: null, tabs: [], }, diff --git a/packages/twenty-front/src/modules/settings/page-layout/states/savedPageLayoutsState.ts b/packages/twenty-front/src/modules/settings/page-layout/states/savedPageLayoutsState.ts index 29a2a506c29..c73da73da89 100644 --- a/packages/twenty-front/src/modules/settings/page-layout/states/savedPageLayoutsState.ts +++ b/packages/twenty-front/src/modules/settings/page-layout/states/savedPageLayoutsState.ts @@ -43,7 +43,6 @@ export type SavedPageLayout = { id: string; name: string; type: PageLayoutType; - workspaceId?: string; objectMetadataId?: string | null; tabs: PageLayoutTab[]; createdAt: string; diff --git a/packages/twenty-front/src/modules/settings/page-layout/utils/__tests__/convertLayoutsToWidgets.test.ts b/packages/twenty-front/src/modules/settings/page-layout/utils/__tests__/convertLayoutsToWidgets.test.ts index b697a866a9c..fc917b473aa 100644 --- a/packages/twenty-front/src/modules/settings/page-layout/utils/__tests__/convertLayoutsToWidgets.test.ts +++ b/packages/twenty-front/src/modules/settings/page-layout/utils/__tests__/convertLayoutsToWidgets.test.ts @@ -1,4 +1,4 @@ -import { GraphSubType, WidgetType } from '../../mocks/mockWidgets'; +import { GraphType, WidgetType } from '../../mocks/mockWidgets'; import { type PageLayoutWidget } from '../../states/savedPageLayoutsState'; import { convertLayoutsToWidgets } from '../convertLayoutsToWidgets'; @@ -17,7 +17,7 @@ describe('convertLayoutsToWidgets', () => { columnSpan: 2, }, configuration: { - graphType: GraphSubType.NUMBER, + graphType: GraphType.NUMBER, }, data: { value: 100 }, createdAt: '2024-01-01T00:00:00Z', @@ -37,7 +37,7 @@ describe('convertLayoutsToWidgets', () => { columnSpan: 2, }, configuration: { - graphType: GraphSubType.PIE, + graphType: GraphType.PIE, }, data: { items: [] }, createdAt: '2024-01-01T00:00:00Z', diff --git a/packages/twenty-front/src/modules/settings/page-layout/utils/getDefaultWidgetData.ts b/packages/twenty-front/src/modules/settings/page-layout/utils/getDefaultWidgetData.ts index 6370e87eeda..ef12e54118e 100644 --- a/packages/twenty-front/src/modules/settings/page-layout/utils/getDefaultWidgetData.ts +++ b/packages/twenty-front/src/modules/settings/page-layout/utils/getDefaultWidgetData.ts @@ -1,14 +1,14 @@ -import { GraphSubType } from '../mocks/mockWidgets'; +import { GraphType } from '../mocks/mockWidgets'; -export const getDefaultWidgetData = (graphType: GraphSubType) => { +export const getDefaultWidgetData = (graphType: GraphType) => { switch (graphType) { - case GraphSubType.NUMBER: + case GraphType.NUMBER: return { value: '1,234', trendPercentage: 15.2, }; - case GraphSubType.GAUGE: + case GraphType.GAUGE: return { value: 0.7, min: 0, @@ -16,7 +16,7 @@ export const getDefaultWidgetData = (graphType: GraphSubType) => { label: 'Progress', }; - case GraphSubType.PIE: + case GraphType.PIE: return { items: [ { id: 'segment1', value: 35, label: 'Segment A' }, @@ -26,7 +26,7 @@ export const getDefaultWidgetData = (graphType: GraphSubType) => { ], }; - case GraphSubType.BAR: + case GraphType.BAR: return { items: [ { category: 'Jan', value: 45 }, @@ -46,29 +46,26 @@ export const getDefaultWidgetData = (graphType: GraphSubType) => { } }; -export const getWidgetTitle = ( - graphType: GraphSubType, - index: number, -): string => { - const baseNames: Record = { - [GraphSubType.NUMBER]: 'Number', - [GraphSubType.GAUGE]: 'Gauge', - [GraphSubType.PIE]: 'Pie Chart', - [GraphSubType.BAR]: 'Bar Chart', +export const getWidgetTitle = (graphType: GraphType, index: number): string => { + const baseNames: Record = { + [GraphType.NUMBER]: 'Number', + [GraphType.GAUGE]: 'Gauge', + [GraphType.PIE]: 'Pie Chart', + [GraphType.BAR]: 'Bar Chart', }; return `${baseNames[graphType] || 'Widget'} ${index + 1}`; }; -export const getWidgetSize = (graphType: GraphSubType) => { +export const getWidgetSize = (graphType: GraphType) => { switch (graphType) { - case GraphSubType.NUMBER: + case GraphType.NUMBER: return { w: 3, h: 2 }; - case GraphSubType.GAUGE: + case GraphType.GAUGE: return { w: 3, h: 3 }; - case GraphSubType.PIE: + case GraphType.PIE: return { w: 4, h: 4 }; - case GraphSubType.BAR: + case GraphType.BAR: return { w: 6, h: 4 }; default: return { w: 4, h: 4 }; diff --git a/packages/twenty-front/src/modules/settings/page-layout/utils/graphRegistry.tsx b/packages/twenty-front/src/modules/settings/page-layout/utils/graphRegistry.tsx deleted file mode 100644 index c9a8547678c..00000000000 --- a/packages/twenty-front/src/modules/settings/page-layout/utils/graphRegistry.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { GraphWidgetBarChart } from '@/dashboards/widgets/graph/components/GraphWidgetBarChart'; -import { GraphWidgetGaugeChart } from '@/dashboards/widgets/graph/components/GraphWidgetGaugeChart'; -import { GraphWidgetNumberChart } from '@/dashboards/widgets/graph/components/GraphWidgetNumberChart'; -import { GraphWidgetPieChart } from '@/dashboards/widgets/graph/components/GraphWidgetPieChart'; -import { type ReactNode } from 'react'; -import { GraphSubType } from '../mocks/mockWidgets'; -import { type PageLayoutWidget } from '../states/savedPageLayoutsState'; - -type GraphRenderer = (widget: PageLayoutWidget) => ReactNode; - -const graphRenderers: Record = { - [GraphSubType.NUMBER]: (widget) => ( - - ), - [GraphSubType.GAUGE]: (widget) => ( - - ), - [GraphSubType.PIE]: (widget) => ( - - ), - [GraphSubType.BAR]: (widget) => ( - - ), -}; - -export const renderGraphWidget = (widget: PageLayoutWidget): ReactNode => { - const graphType = widget.configuration?.graphType; - - if (!graphType || typeof graphType !== 'string') { - return null; - } - - if (!Object.values(GraphSubType).includes(graphType as GraphSubType)) { - return null; - } - - const renderer = graphRenderers[graphType as GraphSubType]; - if (!renderer) { - return null; - } - - return renderer(widget); -}; diff --git a/packages/twenty-front/src/modules/ui/layout/tab-list/components/__stories__/Tablist.stories.tsx b/packages/twenty-front/src/modules/ui/layout/tab-list/components/__stories__/Tablist.stories.tsx index 7bc98465905..bd6c3766ceb 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab-list/components/__stories__/Tablist.stories.tsx +++ b/packages/twenty-front/src/modules/ui/layout/tab-list/components/__stories__/Tablist.stories.tsx @@ -1,5 +1,6 @@ import styled from '@emotion/styled'; import { type Meta, type StoryObj } from '@storybook/react'; +import { useState } from 'react'; import { IconCalendar, IconCheckbox, @@ -11,6 +12,8 @@ import { } from 'twenty-ui/display'; import { ComponentWithRouterDecorator } from 'twenty-ui/testing'; import { TabList } from '../TabList'; +import { type TabListProps } from '../../types/TabListProps'; +import { type SingleTabProps } from '../../types/SingleTabProps'; const tabs = [ { id: 'general', title: 'General', logo: 'https://picsum.photos/200' }, @@ -80,3 +83,72 @@ export const Default: Story = { ), }; + +type TabListWithAddProps = Pick< + TabListProps, + 'componentInstanceId' | 'loading' | 'isInRightDrawer' | 'className' +> & { + initialTabs: SingleTabProps[]; +}; + +const TabListWithAdd = ({ + componentInstanceId, + loading, + isInRightDrawer, + className, + initialTabs, +}: TabListWithAddProps) => { + const [currentTabs, setCurrentTabs] = useState(initialTabs); + const [nextTabId, setNextTabId] = useState(initialTabs.length + 1); + + const handleAddTab = () => { + const newTab: SingleTabProps = { + id: `new-tab-${nextTabId}`, + title: `New Tab ${nextTabId}`, + Icon: IconCheckbox, + }; + setCurrentTabs([...currentTabs, newTab]); + setNextTabId(nextTabId + 1); + }; + + return ( + +

+ Click the + button to add new tabs! +

+ +
+ ); +}; + +export const WithAddTab: Story = { + args: { + componentInstanceId: 'tabs-with-add', + tabs: tabs.slice(0, 3), + }, + render: (args) => ( + + ), + parameters: { + docs: { + description: { + story: + 'TabList with the ability to add new tabs dynamically using the onAddTab callback. Click the + button to add new tabs.', + }, + }, + }, +}; diff --git a/packages/twenty-front/src/modules/ui/layout/tab-list/utils/calculateVisibleTabCount.ts b/packages/twenty-front/src/modules/ui/layout/tab-list/utils/calculateVisibleTabCount.ts index 72e4d4ed35a..fad27106297 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab-list/utils/calculateVisibleTabCount.ts +++ b/packages/twenty-front/src/modules/ui/layout/tab-list/utils/calculateVisibleTabCount.ts @@ -2,6 +2,7 @@ import { TAB_LIST_GAP } from '@/ui/layout/tab-list/constants/TabListGap'; import { TAB_LIST_LEFT_PADDING } from '@/ui/layout/tab-list/constants/TabListPadding'; import { type SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps'; import { type TabWidthsById } from '@/ui/layout/tab-list/types/TabWidthsById'; +import { isDefined } from 'twenty-shared/utils'; type CalculateVisibleTabCountParams = { visibleTabs: SingleTabProps[]; @@ -22,7 +23,6 @@ export const calculateVisibleTabCount = ({ return visibleTabs.length; } - // Subtract add button width if present const availableWidth = containerWidth - TAB_LIST_LEFT_PADDING - @@ -33,8 +33,7 @@ export const calculateVisibleTabCount = ({ const tab = visibleTabs[i]; const tabWidth = tabWidthsById[tab.id]; - // Skip if width not measured yet - if (tabWidth === undefined) { + if (!isDefined(tabWidth)) { return visibleTabs.length; } diff --git a/packages/twenty-front/src/pages/settings/page-layout/SettingsPageLayoutEdit.tsx b/packages/twenty-front/src/pages/settings/page-layout/SettingsPageLayoutEdit.tsx index a8effe2c50a..50c07264151 100644 --- a/packages/twenty-front/src/pages/settings/page-layout/SettingsPageLayoutEdit.tsx +++ b/packages/twenty-front/src/pages/settings/page-layout/SettingsPageLayoutEdit.tsx @@ -4,27 +4,29 @@ import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons import { SettingsPageFullWidthContainer } from '@/settings/components/SettingsPageFullWidthContainer'; import { PageLayoutInitializationEffect } from '@/settings/page-layout/components/PageLayoutInitializationEffect'; import { PageLayoutWidgetPlaceholder } from '@/settings/page-layout/components/PageLayoutWidgetPlaceholder'; +import { WidgetRenderer } from '@/settings/page-layout/components/WidgetRenderer'; import { EMPTY_LAYOUT } from '@/settings/page-layout/constants/EmptyLayout'; import { PAGE_LAYOUT_CONFIG, type PageLayoutBreakpoint, } from '@/settings/page-layout/constants/PageLayoutBreakpoints'; +import { SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID } from '@/settings/page-layout/constants/SettingsPageLayoutTabsInstanceId'; +import { useChangePageLayoutDragSelection } from '@/settings/page-layout/hooks/useChangePageLayoutDragSelection'; +import { useCreatePageLayoutTab } from '@/settings/page-layout/hooks/useCreatePageLayoutTab'; +import { useDeletePageLayoutWidget } from '@/settings/page-layout/hooks/useDeletePageLayoutWidget'; +import { useEndPageLayoutDragSelection } from '@/settings/page-layout/hooks/useEndPageLayoutDragSelection'; import { usePageLayoutDraftState } from '@/settings/page-layout/hooks/usePageLayoutDraftState'; -import { usePageLayoutDragSelection } from '@/settings/page-layout/hooks/usePageLayoutDragSelection'; import { usePageLayoutHandleLayoutChange } from '@/settings/page-layout/hooks/usePageLayoutHandleLayoutChange'; import { usePageLayoutSaveHandler } from '@/settings/page-layout/hooks/usePageLayoutSaveHandler'; -import { usePageLayoutTabCreate } from '@/settings/page-layout/hooks/usePageLayoutTabCreate'; -import { usePageLayoutWidgetDelete } from '@/settings/page-layout/hooks/usePageLayoutWidgetDelete'; +import { useStartPageLayoutDragSelection } from '@/settings/page-layout/hooks/useStartPageLayoutDragSelection'; import { WidgetType } from '@/settings/page-layout/mocks/mockWidgets'; import { pageLayoutCurrentBreakpointState } from '@/settings/page-layout/states/pageLayoutCurrentBreakpointState'; import { pageLayoutCurrentLayoutsState } from '@/settings/page-layout/states/pageLayoutCurrentLayoutsState'; -import { pageLayoutCurrentTabIdForCreationState } from '@/settings/page-layout/states/pageLayoutCurrentTabIdForCreation'; import { pageLayoutEditingWidgetIdState } from '@/settings/page-layout/states/pageLayoutEditingWidgetIdState'; import { pageLayoutSelectedCellsState } from '@/settings/page-layout/states/pageLayoutSelectedCellsState'; import { type PageLayoutWidget } from '@/settings/page-layout/states/savedPageLayoutsState'; import { calculateTotalGridRows } from '@/settings/page-layout/utils/calculateTotalGridRows'; import { generateCellId } from '@/settings/page-layout/utils/generateCellId'; -import { renderWidget } from '@/settings/page-layout/utils/widgetRegistry'; import { SettingsPath } from '@/types/SettingsPath'; import { TitleInput } from '@/ui/input/components/TitleInput'; import { TabList } from '@/ui/layout/tab-list/components/TabList'; @@ -143,9 +145,6 @@ export const SettingsPageLayoutEdit = () => { const pageLayoutCurrentLayouts = useRecoilValue( pageLayoutCurrentLayoutsState, ); - const setPageLayoutCurrentTabIdForCreation = useSetRecoilState( - pageLayoutCurrentTabIdForCreationState, - ); const { navigateCommandMenu } = useNavigateCommandMenu(); const setPageLayoutEditingWidgetId = useSetRecoilState( pageLayoutEditingWidgetIdState, @@ -155,7 +154,13 @@ export const SettingsPageLayoutEdit = () => { const activeTabId = useRecoilComponentValue( activeTabIdComponentState, - 'page-layout-tabs', + SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID, + ); + + const setActiveTabId = useSetRecoilState( + activeTabIdComponentState.atomFamily({ + instanceId: SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID, + }), ); const activeTabWidgets = useMemo(() => { @@ -171,25 +176,22 @@ export const SettingsPageLayoutEdit = () => { [pageLayoutDraft.tabs], ); - const { - handleDragSelectionStart, - handleDragSelectionChange, - handleDragSelectionEnd, - } = usePageLayoutDragSelection(); + const { startPageLayoutDragSelection } = useStartPageLayoutDragSelection(); + const { changePageLayoutDragSelection } = useChangePageLayoutDragSelection(); + const { endPageLayoutDragSelection } = useEndPageLayoutDragSelection(); const handleOpenAddWidget = useCallback(() => { - setPageLayoutCurrentTabIdForCreation(activeTabId); navigateCommandMenu({ page: CommandMenuPages.PageLayoutWidgetTypeSelect, pageTitle: 'Add Widget', pageIcon: IconAppWindow, resetNavigationStack: true, }); - }, [navigateCommandMenu, activeTabId, setPageLayoutCurrentTabIdForCreation]); + }, [navigateCommandMenu]); - const { handleRemoveWidget } = usePageLayoutWidgetDelete(); + const { deletePageLayoutWidget } = useDeletePageLayoutWidget(); const { handleLayoutChange } = usePageLayoutHandleLayoutChange(activeTabId); - const { handleCreateTab } = usePageLayoutTabCreate(); + const { createPageLayoutTab } = useCreatePageLayoutTab(); const handleEditWidget = useCallback( (widgetId: string) => { @@ -197,9 +199,6 @@ export const SettingsPageLayoutEdit = () => { if (!widget) return; setPageLayoutEditingWidgetId(widgetId); - setPageLayoutCurrentTabIdForCreation( - widget.pageLayoutTabId || activeTabId, - ); if (widget.type === WidgetType.IFRAME) { navigateCommandMenu({ @@ -210,13 +209,7 @@ export const SettingsPageLayoutEdit = () => { }); } }, - [ - allWidgets, - setPageLayoutEditingWidgetId, - navigateCommandMenu, - setPageLayoutCurrentTabIdForCreation, - activeTabId, - ], + [allWidgets, setPageLayoutEditingWidgetId, navigateCommandMenu], ); const isEmptyState = activeTabWidgets.length === 0; @@ -233,16 +226,10 @@ export const SettingsPageLayoutEdit = () => { navigateSettings(SettingsPath.PageLayout); }; - const setActiveTabId = useSetRecoilState( - activeTabIdComponentState.atomFamily({ - instanceId: 'page-layout-tabs', - }), - ); - const handleAddTab = useCallback(() => { - const newTabId = handleCreateTab(); + const newTabId = createPageLayoutTab(); setActiveTabId(newTabId); - }, [handleCreateTab, setActiveTabId]); + }, [createPageLayoutTab, setActiveTabId]); const tabListTabs: SingleTabProps[] = useMemo(() => { return [...pageLayoutDraft.tabs] @@ -334,7 +321,7 @@ export const SettingsPageLayoutEdit = () => { )} @@ -394,10 +381,10 @@ export const SettingsPageLayoutEdit = () => {
handleRemoveWidget(widget.id)} + onRemove={() => deletePageLayoutWidget(widget.id)} onEdit={() => handleEditWidget(widget.id)} > - {renderWidget(widget)} +
)) @@ -406,9 +393,9 @@ export const SettingsPageLayoutEdit = () => { {pageLayoutCurrentBreakpoint !== 'mobile' && ( )}