Prevent csv export injections (#14347)
**Small Security Issue:** CSV exports were vulnerable to formula injection attacks when users entered values starting with =, +, -, or @. (only happens if a logged-in user injects corrupted data) Solution: - Added ZWJ (Zero-Width Joiner) protection that prefixes dangerous values with invisible Unicode character - This is the best way to preserve original data while preventing Excel from executing formulas - Added import cleanup to restore original values when re-importing Changes: - New sanitizeValueForCSVExport() function for security - Updated all CSV export paths to use both security + formatting functions - Added comprehensive tests covering attack vectors and international characters - Also added cursor rules for better code consistency --------- Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
This commit is contained in:
-139
@@ -1,139 +0,0 @@
|
||||
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',
|
||||
);
|
||||
});
|
||||
});
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
-208
@@ -1,208 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
+5
-2
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
GraphType,
|
||||
GraphSubType,
|
||||
WidgetType,
|
||||
} from '@/settings/page-layout/mocks/mockWidgets';
|
||||
import { PageLayoutType } from '@/settings/page-layout/states/savedPageLayoutsState';
|
||||
@@ -26,6 +26,7 @@ describe('usePageLayoutDraftState', () => {
|
||||
result.current.setPageLayoutDraft({
|
||||
name: ' ',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
workspaceId: undefined,
|
||||
objectMetadataId: null,
|
||||
tabs: [],
|
||||
});
|
||||
@@ -44,6 +45,7 @@ describe('usePageLayoutDraftState', () => {
|
||||
result.current.setPageLayoutDraft({
|
||||
name: 'Updated Name',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
workspaceId: undefined,
|
||||
objectMetadataId: null,
|
||||
tabs: [],
|
||||
});
|
||||
@@ -63,6 +65,7 @@ describe('usePageLayoutDraftState', () => {
|
||||
result.current.setPageLayoutDraft({
|
||||
name: 'Test Layout',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
workspaceId: undefined,
|
||||
objectMetadataId: null,
|
||||
tabs: [
|
||||
{
|
||||
@@ -80,7 +83,7 @@ describe('usePageLayoutDraftState', () => {
|
||||
title: 'New Widget',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 2, column: 2, rowSpan: 2, columnSpan: 2 },
|
||||
configuration: { graphType: GraphType.BAR },
|
||||
configuration: { graphType: GraphSubType.BAR },
|
||||
data: {},
|
||||
objectMetadataId: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
+12
-12
@@ -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 { useCreatePageLayoutTab } from '../useCreatePageLayoutTab';
|
||||
import { usePageLayoutTabCreate } from '../usePageLayoutTabCreate';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('useCreatePageLayoutTab', () => {
|
||||
describe('usePageLayoutTabCreate', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
@@ -18,7 +18,7 @@ describe('useCreatePageLayoutTab', () => {
|
||||
uuidModule.v4.mockReturnValue('mock-uuid');
|
||||
const { result } = renderHook(
|
||||
() => ({
|
||||
createTab: useCreatePageLayoutTab(),
|
||||
createTab: usePageLayoutTabCreate(),
|
||||
pageLayoutCurrentLayouts: useRecoilValue(pageLayoutCurrentLayoutsState),
|
||||
pageLayoutDraft: useRecoilValue(pageLayoutDraftState),
|
||||
}),
|
||||
@@ -29,7 +29,7 @@ describe('useCreatePageLayoutTab', () => {
|
||||
|
||||
let newTabId: string;
|
||||
act(() => {
|
||||
newTabId = result.current.createTab.createPageLayoutTab();
|
||||
newTabId = result.current.createTab.handleCreateTab();
|
||||
});
|
||||
|
||||
expect(result.current.pageLayoutDraft.tabs[0].id).toBe('tab-mock-uuid');
|
||||
@@ -50,7 +50,7 @@ describe('useCreatePageLayoutTab', () => {
|
||||
uuidModule.v4.mockReturnValue('mock-uuid');
|
||||
const { result } = renderHook(
|
||||
() => ({
|
||||
createTab: useCreatePageLayoutTab(),
|
||||
createTab: usePageLayoutTabCreate(),
|
||||
pageLayoutDraft: useRecoilValue(pageLayoutDraftState),
|
||||
}),
|
||||
{
|
||||
@@ -59,7 +59,7 @@ describe('useCreatePageLayoutTab', () => {
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.createTab.createPageLayoutTab('Custom Tab Name');
|
||||
result.current.createTab.handleCreateTab('Custom Tab Name');
|
||||
});
|
||||
|
||||
expect(result.current.pageLayoutDraft.tabs[0].title).toBe(
|
||||
@@ -74,7 +74,7 @@ describe('useCreatePageLayoutTab', () => {
|
||||
.mockReturnValueOnce('mock-uuid-2');
|
||||
const { result } = renderHook(
|
||||
() => ({
|
||||
createTab: useCreatePageLayoutTab(),
|
||||
createTab: usePageLayoutTabCreate(),
|
||||
pageLayoutDraft: useRecoilValue(pageLayoutDraftState),
|
||||
}),
|
||||
{
|
||||
@@ -83,11 +83,11 @@ describe('useCreatePageLayoutTab', () => {
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.createTab.createPageLayoutTab();
|
||||
result.current.createTab.handleCreateTab();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.createTab.createPageLayoutTab();
|
||||
result.current.createTab.handleCreateTab();
|
||||
});
|
||||
|
||||
expect(result.current.pageLayoutDraft.tabs).toHaveLength(2);
|
||||
@@ -104,7 +104,7 @@ describe('useCreatePageLayoutTab', () => {
|
||||
.mockReturnValueOnce('mock-uuid-2');
|
||||
const { result } = renderHook(
|
||||
() => ({
|
||||
createTab: useCreatePageLayoutTab(),
|
||||
createTab: usePageLayoutTabCreate(),
|
||||
pageLayoutCurrentLayouts: useRecoilValue(pageLayoutCurrentLayoutsState),
|
||||
}),
|
||||
{
|
||||
@@ -114,12 +114,12 @@ describe('useCreatePageLayoutTab', () => {
|
||||
|
||||
let tabId1: string = '';
|
||||
act(() => {
|
||||
tabId1 = result.current.createTab.createPageLayoutTab();
|
||||
tabId1 = result.current.createTab.handleCreateTab();
|
||||
});
|
||||
|
||||
let tabId2: string = '';
|
||||
act(() => {
|
||||
tabId2 = result.current.createTab.createPageLayoutTab();
|
||||
tabId2 = result.current.createTab.handleCreateTab();
|
||||
});
|
||||
|
||||
expect(result.current.pageLayoutCurrentLayouts[tabId1]).toEqual({
|
||||
+23
-51
@@ -1,21 +1,20 @@
|
||||
import { SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID } from '@/settings/page-layout/constants/SettingsPageLayoutTabsInstanceId';
|
||||
import {
|
||||
GraphType,
|
||||
GraphSubType,
|
||||
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 { useCreatePageLayoutWidget } from '../useCreatePageLayoutWidget';
|
||||
import { usePageLayoutWidgetCreate } from '../usePageLayoutWidgetCreate';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn(() => 'mock-uuid'),
|
||||
}));
|
||||
|
||||
describe('useCreatePageLayoutWidget', () => {
|
||||
describe('usePageLayoutWidgetCreate', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
@@ -24,9 +23,7 @@ describe('useCreatePageLayoutWidget', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const setActiveTabId = useSetRecoilState(
|
||||
activeTabIdComponentState.atomFamily({
|
||||
instanceId: SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID,
|
||||
}),
|
||||
pageLayoutCurrentTabIdForCreationState,
|
||||
);
|
||||
const setPageLayoutDraft = useSetRecoilState(pageLayoutDraftState);
|
||||
const pageLayoutDraft = useRecoilValue(pageLayoutDraftState);
|
||||
@@ -34,7 +31,7 @@ describe('useCreatePageLayoutWidget', () => {
|
||||
const pageLayoutCurrentLayouts = useRecoilValue(
|
||||
pageLayoutCurrentLayoutsState,
|
||||
);
|
||||
const createWidget = useCreatePageLayoutWidget();
|
||||
const createWidget = usePageLayoutWidgetCreate();
|
||||
return {
|
||||
setActiveTabId,
|
||||
setPageLayoutDraft,
|
||||
@@ -52,6 +49,7 @@ describe('useCreatePageLayoutWidget', () => {
|
||||
result.current.setPageLayoutDraft({
|
||||
name: 'Test Layout',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
workspaceId: undefined,
|
||||
objectMetadataId: null,
|
||||
tabs: [
|
||||
{
|
||||
@@ -70,9 +68,9 @@ describe('useCreatePageLayoutWidget', () => {
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.createWidget.createPageLayoutWidget(
|
||||
result.current.createWidget.handleCreateWidget(
|
||||
WidgetType.GRAPH,
|
||||
GraphType.BAR,
|
||||
GraphSubType.BAR,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -91,22 +89,12 @@ describe('useCreatePageLayoutWidget', () => {
|
||||
() => {
|
||||
const setPageLayoutDraft = useSetRecoilState(pageLayoutDraftState);
|
||||
const setActiveTabId = useSetRecoilState(
|
||||
activeTabIdComponentState.atomFamily({
|
||||
instanceId: SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID,
|
||||
}),
|
||||
pageLayoutCurrentTabIdForCreationState,
|
||||
);
|
||||
const pageLayoutDraft = useRecoilValue(pageLayoutDraftState);
|
||||
const allWidgets = pageLayoutDraft.tabs.flatMap((tab) => tab.widgets);
|
||||
const pageLayoutCurrentLayouts = useRecoilValue(
|
||||
pageLayoutCurrentLayoutsState,
|
||||
);
|
||||
const createWidget = useCreatePageLayoutWidget();
|
||||
const createWidget = usePageLayoutWidgetCreate();
|
||||
return {
|
||||
setPageLayoutDraft,
|
||||
setActiveTabId,
|
||||
pageLayoutDraft,
|
||||
allWidgets,
|
||||
pageLayoutCurrentLayouts,
|
||||
createWidget,
|
||||
};
|
||||
},
|
||||
@@ -119,6 +107,7 @@ describe('useCreatePageLayoutWidget', () => {
|
||||
result.current.setPageLayoutDraft({
|
||||
name: 'Test Layout',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
workspaceId: undefined,
|
||||
objectMetadataId: null,
|
||||
tabs: [
|
||||
{
|
||||
@@ -137,41 +126,24 @@ describe('useCreatePageLayoutWidget', () => {
|
||||
});
|
||||
|
||||
const graphTypes = [
|
||||
GraphType.NUMBER,
|
||||
GraphType.GAUGE,
|
||||
GraphType.PIE,
|
||||
GraphType.BAR,
|
||||
GraphSubType.NUMBER,
|
||||
GraphSubType.GAUGE,
|
||||
GraphSubType.PIE,
|
||||
GraphSubType.BAR,
|
||||
];
|
||||
|
||||
graphTypes.forEach((graphType) => {
|
||||
act(() => {
|
||||
result.current.createWidget.createPageLayoutWidget(
|
||||
result.current.createWidget.handleCreateWidget(
|
||||
WidgetType.GRAPH,
|
||||
graphType,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
expect(typeof result.current.createWidget.handleCreateWidget).toBe(
|
||||
'function',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not create widget when activeTabId is null', () => {
|
||||
@@ -182,7 +154,7 @@ describe('useCreatePageLayoutWidget', () => {
|
||||
const pageLayoutCurrentLayouts = useRecoilValue(
|
||||
pageLayoutCurrentLayoutsState,
|
||||
);
|
||||
const createWidget = useCreatePageLayoutWidget();
|
||||
const createWidget = usePageLayoutWidgetCreate();
|
||||
return { allWidgets, pageLayoutCurrentLayouts, createWidget };
|
||||
},
|
||||
{
|
||||
@@ -191,9 +163,9 @@ describe('useCreatePageLayoutWidget', () => {
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.createWidget.createPageLayoutWidget(
|
||||
result.current.createWidget.handleCreateWidget(
|
||||
WidgetType.GRAPH,
|
||||
GraphType.BAR,
|
||||
GraphSubType.BAR,
|
||||
);
|
||||
});
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
-73
@@ -1,73 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
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 };
|
||||
};
|
||||
+6
-9
@@ -1,10 +1,8 @@
|
||||
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';
|
||||
@@ -13,11 +11,6 @@ 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) => {
|
||||
@@ -28,6 +21,10 @@ export const useCreatePageLayoutIframeWidget = () => {
|
||||
.getLoadable(pageLayoutDraggedAreaState)
|
||||
.getValue();
|
||||
|
||||
const activeTabId = snapshot
|
||||
.getLoadable(pageLayoutCurrentTabIdForCreationState)
|
||||
.getValue();
|
||||
|
||||
if (!activeTabId) {
|
||||
return;
|
||||
}
|
||||
@@ -81,7 +78,7 @@ export const useCreatePageLayoutIframeWidget = () => {
|
||||
|
||||
set(pageLayoutDraggedAreaState, null);
|
||||
},
|
||||
[activeTabId],
|
||||
[],
|
||||
);
|
||||
|
||||
return { createPageLayoutIframeWidget };
|
||||
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
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 };
|
||||
};
|
||||
+1
@@ -12,6 +12,7 @@ export const usePageLayoutDraftState = () => {
|
||||
? !isDeeplyEqual(pageLayoutDraft, {
|
||||
name: pageLayoutPersisted.name,
|
||||
type: pageLayoutPersisted.type,
|
||||
workspaceId: pageLayoutPersisted.workspaceId,
|
||||
objectMetadataId: pageLayoutPersisted.objectMetadataId,
|
||||
tabs: pageLayoutPersisted.tabs,
|
||||
})
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
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,
|
||||
};
|
||||
};
|
||||
+5
-6
@@ -1,9 +1,7 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
import { pageLayoutDraftState } from '../states/pageLayoutDraftState';
|
||||
import { pageLayoutPersistedState } from '../states/pageLayoutPersistedState';
|
||||
import {
|
||||
@@ -13,7 +11,7 @@ import {
|
||||
} from '../states/savedPageLayoutsState';
|
||||
|
||||
export const usePageLayoutSaveHandler = () => {
|
||||
const navigateSettings = useNavigateSettings();
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const isEditMode = id && id !== 'new';
|
||||
|
||||
@@ -44,6 +42,7 @@ export const usePageLayoutSaveHandler = () => {
|
||||
id: isEditMode ? id : uuidv4(),
|
||||
name: pageLayoutDraft.name,
|
||||
type: pageLayoutDraft.type,
|
||||
workspaceId: pageLayoutDraft.workspaceId,
|
||||
objectMetadataId: pageLayoutDraft.objectMetadataId,
|
||||
tabs: updatedTabs,
|
||||
createdAt: isEditMode
|
||||
@@ -64,9 +63,9 @@ export const usePageLayoutSaveHandler = () => {
|
||||
|
||||
set(pageLayoutPersistedState, layoutToSave);
|
||||
|
||||
navigateSettings(SettingsPath.PageLayout);
|
||||
navigate('/settings/page-layout');
|
||||
},
|
||||
[isEditMode, id, navigateSettings],
|
||||
[isEditMode, id, navigate],
|
||||
);
|
||||
|
||||
return { savePageLayout };
|
||||
|
||||
+3
-3
@@ -5,8 +5,8 @@ import { pageLayoutDraftState } from '../states/pageLayoutDraftState';
|
||||
import { type PageLayoutTab } from '../states/savedPageLayoutsState';
|
||||
import { createEmptyTabLayout } from '../utils/createEmptyTabLayout';
|
||||
|
||||
export const useCreatePageLayoutTab = () => {
|
||||
const createPageLayoutTab = useRecoilCallback(
|
||||
export const usePageLayoutTabCreate = () => {
|
||||
const handleCreateTab = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(title?: string): string => {
|
||||
const pageLayoutDraft = snapshot
|
||||
@@ -41,5 +41,5 @@ export const useCreatePageLayoutTab = () => {
|
||||
[],
|
||||
);
|
||||
|
||||
return { createPageLayoutTab };
|
||||
return { handleCreateTab };
|
||||
};
|
||||
+10
-14
@@ -1,10 +1,8 @@
|
||||
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 { SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID } from '../constants/SettingsPageLayoutTabsInstanceId';
|
||||
import { type GraphType, type WidgetType } from '../mocks/mockWidgets';
|
||||
import { type GraphSubType, 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';
|
||||
@@ -17,15 +15,10 @@ import {
|
||||
} from '../utils/getDefaultWidgetData';
|
||||
import { getDefaultWidgetPosition } from '../utils/getDefaultWidgetPosition';
|
||||
|
||||
export const useCreatePageLayoutWidget = () => {
|
||||
const activeTabId = useRecoilComponentValue(
|
||||
activeTabIdComponentState,
|
||||
SETTINGS_PAGE_LAYOUT_TABS_INSTANCE_ID,
|
||||
);
|
||||
|
||||
const createPageLayoutWidget = useRecoilCallback(
|
||||
export const usePageLayoutWidgetCreate = () => {
|
||||
const handleCreateWidget = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(widgetType: WidgetType, graphType: GraphType) => {
|
||||
(widgetType: WidgetType, graphType: GraphSubType) => {
|
||||
const widgetData = getDefaultWidgetData(graphType);
|
||||
|
||||
const pageLayoutDraft = snapshot
|
||||
@@ -37,6 +30,9 @@ export const useCreatePageLayoutWidget = () => {
|
||||
const pageLayoutDraggedArea = snapshot
|
||||
.getLoadable(pageLayoutDraggedAreaState)
|
||||
.getValue();
|
||||
const activeTabId = snapshot
|
||||
.getLoadable(pageLayoutCurrentTabIdForCreationState)
|
||||
.getValue();
|
||||
|
||||
if (!activeTabId) {
|
||||
return;
|
||||
@@ -99,8 +95,8 @@ export const useCreatePageLayoutWidget = () => {
|
||||
|
||||
set(pageLayoutDraggedAreaState, null);
|
||||
},
|
||||
[activeTabId],
|
||||
[],
|
||||
);
|
||||
|
||||
return { createPageLayoutWidget };
|
||||
return { handleCreateWidget };
|
||||
};
|
||||
+4
-5
@@ -1,12 +1,11 @@
|
||||
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 useDeletePageLayoutWidget = () => {
|
||||
const deletePageLayoutWidget = useRecoilCallback(
|
||||
export const usePageLayoutWidgetDelete = () => {
|
||||
const handleRemoveWidget = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(widgetId: string) => {
|
||||
const pageLayoutDraft = snapshot
|
||||
@@ -21,7 +20,7 @@ export const useDeletePageLayoutWidget = () => {
|
||||
);
|
||||
const tabId = tabWithWidget?.id;
|
||||
|
||||
if (isDefined(tabId)) {
|
||||
if (tabId !== undefined) {
|
||||
const updatedLayouts = removeWidgetLayoutFromTab(
|
||||
allTabLayouts,
|
||||
tabId,
|
||||
@@ -38,5 +37,5 @@ export const useDeletePageLayoutWidget = () => {
|
||||
[],
|
||||
);
|
||||
|
||||
return { deletePageLayoutWidget };
|
||||
return { handleRemoveWidget };
|
||||
};
|
||||
+3
-3
@@ -2,8 +2,8 @@ import { useRecoilCallback } from 'recoil';
|
||||
import { type PageLayoutWidget } from '../states/savedPageLayoutsState';
|
||||
import { pageLayoutDraftState } from '../states/pageLayoutDraftState';
|
||||
|
||||
export const useUpdatePageLayoutWidget = () => {
|
||||
const updatePageLayoutWidget = useRecoilCallback(
|
||||
export const usePageLayoutWidgetUpdate = () => {
|
||||
const handleUpdateWidget = useRecoilCallback(
|
||||
({ set }) =>
|
||||
(widgetId: string, updates: Partial<PageLayoutWidget>) => {
|
||||
set(pageLayoutDraftState, (prev) => ({
|
||||
@@ -19,5 +19,5 @@ export const useUpdatePageLayoutWidget = () => {
|
||||
[],
|
||||
);
|
||||
|
||||
return { updatePageLayoutWidget };
|
||||
return { handleUpdateWidget };
|
||||
};
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { pageLayoutSelectedCellsState } from '../states/pageLayoutSelectedCellsState';
|
||||
|
||||
export const useStartPageLayoutDragSelection = () => {
|
||||
const startPageLayoutDragSelection = useRecoilCallback(
|
||||
({ set }) =>
|
||||
() => {
|
||||
set(pageLayoutSelectedCellsState, new Set());
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return { startPageLayoutDragSelection };
|
||||
};
|
||||
Reference in New Issue
Block a user