## Recoil to Jotai Migration — PR 13: Workflow, Page Layout, AI,
Activities, Settings, SSE & Remaining Modules

### Summary

This is the 13th PR in the [14-PR migration
series](packages/twenty-front/src/modules/ui/utilities/state/jotai/MIGRATION_PLAN.md)
to replace Recoil with Jotai as the state management library in
`twenty-front`. This PR migrates the remaining consumer code across all
modules — updating ~1,076 files with ~10k insertions/deletions.

### What changed

**New Jotai infrastructure (13 new files):**
- `createComponentSelectorV2` and `createComponentFamilySelectorV2` —
instance-scoped derived atoms, replacing Recoil's component selectors
- 6 new hooks: `useRecoilComponentSelectorValueV2`,
`useRecoilComponentFamilySelectorValueV2`,
`useRecoilComponentSelectorCallbackStateV2`,
`useRecoilComponentFamilySelectorCallbackStateV2`,
`useRecoilComponentStateCallbackStateV2`,
`useRecoilComponentFamilyStateCallbackStateV2`
- New types: `ComponentSelectorV2`, `ComponentFamilySelectorV2`,
`SelectorCallbacksV2`
- Extended `buildGetHelper` to support the new selector patterns

**State definition migrations:**
- `createState()` → `createStateV2()` (Jotai `atom()`)
- `createFamilyState()` → `createFamilyStateV2()` (Jotai `atom()` with
family cache)
- Recoil `selector`/`selectorFamily` → Jotai derived `atom()` via V2
utilities

**Hook migrations (~2,400 removed, ~1,545 added V2 equivalents):**
- `useRecoilValue` → `useRecoilValueV2`
- `useRecoilState` → `useRecoilStateV2`
- `useSetRecoilState` → `useSetRecoilStateV2`
- `useRecoilCallback` → `useCallback` + `jotaiStore.get/set`
- `useRecoilComponentValue` → `useRecoilComponentValueV2`
- `useSetRecoilComponentState` → `useSetRecoilComponentStateV2`
- `useRecoilComponentFamilyValue` → `useRecoilComponentFamilyValueV2` /
`useFamilySelectorValueV2`
- ~397 direct `import from 'recoil'` removed

**Deleted files (9 files):**
- `dateLocaleState.ts` — consolidated
- `currentAIChatThreadTitleStateV2.ts` — renamed to replace the original
- `isCommandMenuOpenedState.ts` — removed
- `filesFieldUploadState.ts` — replaced by V2
- `recordStoreFamilySelector.ts`, `recordStoreFieldValueSelector.ts`,
`recordStoreRecordsSelector.ts` — replaced by V2 equivalents
- `settingsAllRolesSelector.ts` — replaced by `useSettingsAllRoles` hook
- `settingsRoleIdsStateV2.ts` — consolidated

**Modules migrated:**
- Workflow (diagram, steps, variables, filters)
- Page Layout (widgets, fields, graph, tabs)
- AI (chat, agents, browsing context)
- Activities (rich text editor, targets, timeline)
- Action Menu (single/multiple record actions)
- Command Menu (navigation, history, pages, workflow)
- Context Store
- Record Board, Record Table, Record Calendar, Record Index
- Record Field, Record Filter, Record Sort, Record Group
- Record Drag, Record Picker, Record Store
- Views (view bar, view picker, filters, sorts)
- Settings (roles, permissions, SSO, security, data model, developers,
domains)
- SSE (event streams, subscriptions)
- Spreadsheet Import
- Auth, Favorites, Front Components, Information Banner
This commit is contained in:
Charles Bochet
2026-02-24 17:37:01 +01:00
committed by GitHub
parent c6ec764b23
commit 0f2af6a6cb
1326 changed files with 19248 additions and 19389 deletions
@@ -7,19 +7,16 @@ import {
Provider as JotaiProvider,
} from 'jotai';
import { type ReactNode, useState } from 'react';
import { RecoilRoot, type MutableSnapshot } from 'recoil';
export const PAGE_LAYOUT_TEST_INSTANCE_ID =
'20202020-f244-4ae0-906b-78958aa07642';
export const PageLayoutTestWrapper = ({
children,
initializeState,
instanceId: instanceIdFromProps,
store: storeFromProps,
}: {
children: ReactNode;
initializeState?: (snapshot: MutableSnapshot) => void;
instanceId?: string;
store?: ReturnType<typeof getDefaultStore>;
}) => {
@@ -35,7 +32,7 @@ export const PageLayoutTestWrapper = ({
instanceId: getTabListInstanceIdFromPageLayoutId(instanceId),
}}
>
<RecoilRoot initializeState={initializeState}>{children}</RecoilRoot>
{children}
</TabListComponentInstanceContext.Provider>
</PageLayoutComponentInstanceContext.Provider>
</JotaiProvider>
@@ -1,5 +1,6 @@
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentValueV2';
import { act, renderHook } from '@testing-library/react';
import { createStore } from 'jotai';
import { type ReactNode } from 'react';
import { pageLayoutSelectedCellsComponentState } from '@/page-layout/states/pageLayoutSelectedCellsComponentState';
import {
@@ -10,28 +11,27 @@ import { useChangePageLayoutDragSelection } from '@/page-layout/hooks/useChangeP
describe('useChangePageLayoutDragSelection', () => {
it('should add cell to selection when selected is true', () => {
const store = createStore();
store.set(
pageLayoutSelectedCellsComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
}),
new Set(['cell-1']),
);
const { result } = renderHook(
() => ({
changeDragSelection: useChangePageLayoutDragSelection(
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
selectedCells: useRecoilComponentValue(
selectedCells: useRecoilComponentValueV2(
pageLayoutSelectedCellsComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
}),
{
wrapper: ({ children }: { children: ReactNode }) => (
<PageLayoutTestWrapper
initializeState={({ set }) => {
set(
pageLayoutSelectedCellsComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
}),
new Set(['cell-1']),
);
}}
>
<PageLayoutTestWrapper store={store}>
{children}
</PageLayoutTestWrapper>
),
@@ -54,28 +54,27 @@ describe('useChangePageLayoutDragSelection', () => {
});
it('should remove cell from selection when selected is false', () => {
const store = createStore();
store.set(
pageLayoutSelectedCellsComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
}),
new Set(['cell-1', 'cell-2']),
);
const { result } = renderHook(
() => ({
changeDragSelection: useChangePageLayoutDragSelection(
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
selectedCells: useRecoilComponentValue(
selectedCells: useRecoilComponentValueV2(
pageLayoutSelectedCellsComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
}),
{
wrapper: ({ children }: { children: ReactNode }) => (
<PageLayoutTestWrapper
initializeState={({ set }) => {
set(
pageLayoutSelectedCellsComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
}),
new Set(['cell-1', 'cell-2']),
);
}}
>
<PageLayoutTestWrapper store={store}>
{children}
</PageLayoutTestWrapper>
),
@@ -103,7 +102,7 @@ describe('useChangePageLayoutDragSelection', () => {
changeDragSelection: useChangePageLayoutDragSelection(
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
selectedCells: useRecoilComponentValue(
selectedCells: useRecoilComponentValueV2(
pageLayoutSelectedCellsComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
@@ -133,28 +132,27 @@ describe('useChangePageLayoutDragSelection', () => {
});
it('should handle removing non-existent cell', () => {
const store = createStore();
store.set(
pageLayoutSelectedCellsComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
}),
new Set(['cell-1']),
);
const { result } = renderHook(
() => ({
changeDragSelection: useChangePageLayoutDragSelection(
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
selectedCells: useRecoilComponentValue(
selectedCells: useRecoilComponentValueV2(
pageLayoutSelectedCellsComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
}),
{
wrapper: ({ children }: { children: ReactNode }) => (
<PageLayoutTestWrapper
initializeState={({ set }) => {
set(
pageLayoutSelectedCellsComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
}),
new Set(['cell-1']),
);
}}
>
<PageLayoutTestWrapper store={store}>
{children}
</PageLayoutTestWrapper>
),
@@ -3,8 +3,8 @@ import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pag
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { type GraphWidgetFieldSelection } from '@/page-layout/types/GraphWidgetFieldSelection';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilComponentStateV2';
import { act, renderHook } from '@testing-library/react';
import { useSetAtom } from 'jotai';
import { isDefined } from 'twenty-shared/utils';
@@ -35,16 +35,16 @@ describe('useCreatePageLayoutGraphWidget', () => {
instanceId: `${PAGE_LAYOUT_TEST_INSTANCE_ID}-tab-list`,
}),
);
const setPageLayoutDraft = useSetRecoilComponentState(
const setPageLayoutDraft = useSetRecoilComponentStateV2(
pageLayoutDraftComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
);
const pageLayoutDraft = useRecoilComponentValue(
const pageLayoutDraft = useRecoilComponentValueV2(
pageLayoutDraftComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
);
const allWidgets = pageLayoutDraft.tabs.flatMap((tab) => tab.widgets);
const pageLayoutCurrentLayouts = useRecoilComponentValue(
const pageLayoutCurrentLayouts = useRecoilComponentValueV2(
pageLayoutCurrentLayoutsComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
);
@@ -109,7 +109,7 @@ describe('useCreatePageLayoutGraphWidget', () => {
it('should handle different graph types', () => {
const { result } = renderHook(
() => {
const setPageLayoutDraft = useSetRecoilComponentState(
const setPageLayoutDraft = useSetRecoilComponentStateV2(
pageLayoutDraftComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
);
@@ -118,12 +118,12 @@ describe('useCreatePageLayoutGraphWidget', () => {
instanceId: `${PAGE_LAYOUT_TEST_INSTANCE_ID}-tab-list`,
}),
);
const pageLayoutDraft = useRecoilComponentValue(
const pageLayoutDraft = useRecoilComponentValueV2(
pageLayoutDraftComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
);
const allWidgets = pageLayoutDraft.tabs.flatMap((tab) => tab.widgets);
const pageLayoutCurrentLayouts = useRecoilComponentValue(
const pageLayoutCurrentLayouts = useRecoilComponentValueV2(
pageLayoutCurrentLayoutsComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
);
@@ -2,9 +2,8 @@ import { useCreatePageLayoutTab } from '@/page-layout/hooks/useCreatePageLayoutT
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilComponentStateV2';
import { act, renderHook } from '@testing-library/react';
import { useSetAtom } from 'jotai';
import { PageLayoutType } from '~/generated-metadata/graphql';
@@ -29,11 +28,11 @@ describe('useCreatePageLayoutTab', () => {
const { result } = renderHook(
() => ({
createTab: useCreatePageLayoutTab(PAGE_LAYOUT_TEST_INSTANCE_ID),
pageLayoutDraft: useRecoilComponentValue(
pageLayoutDraft: useRecoilComponentValueV2(
pageLayoutDraftComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
pageLayoutCurrentLayouts: useRecoilComponentValue(
pageLayoutCurrentLayouts: useRecoilComponentValueV2(
pageLayoutCurrentLayoutsComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
@@ -71,7 +70,7 @@ describe('useCreatePageLayoutTab', () => {
const { result } = renderHook(
() => ({
createTab: useCreatePageLayoutTab(PAGE_LAYOUT_TEST_INSTANCE_ID),
pageLayoutDraft: useRecoilComponentValue(
pageLayoutDraft: useRecoilComponentValueV2(
pageLayoutDraftComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
@@ -99,7 +98,7 @@ describe('useCreatePageLayoutTab', () => {
const { result } = renderHook(
() => ({
createTab: useCreatePageLayoutTab(PAGE_LAYOUT_TEST_INSTANCE_ID),
pageLayoutDraft: useRecoilComponentValue(
pageLayoutDraft: useRecoilComponentValueV2(
pageLayoutDraftComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
@@ -135,7 +134,7 @@ describe('useCreatePageLayoutTab', () => {
const { result } = renderHook(
() => ({
createTab: useCreatePageLayoutTab(PAGE_LAYOUT_TEST_INSTANCE_ID),
pageLayoutCurrentLayouts: useRecoilComponentValue(
pageLayoutCurrentLayouts: useRecoilComponentValueV2(
pageLayoutCurrentLayoutsComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
@@ -204,11 +203,11 @@ describe('useCreatePageLayoutTab', () => {
const { result } = renderHook(
() => {
const setPageLayoutDraft = useSetRecoilComponentState(
const setPageLayoutDraft = useSetRecoilComponentStateV2(
pageLayoutDraftComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
);
const pageLayoutDraft = useRecoilComponentValue(
const pageLayoutDraft = useRecoilComponentValueV2(
pageLayoutDraftComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
);
@@ -2,7 +2,7 @@ import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layo
import { useCreateWidgetFromClick } from '@/page-layout/hooks/useCreateWidgetFromClick';
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentValueV2';
import { act, renderHook } from '@testing-library/react';
import { type ReactNode } from 'react';
import { CommandMenuPages } from 'twenty-shared/types';
@@ -29,11 +29,11 @@ describe('useCreateWidgetFromClick', () => {
const { result } = renderHook(
() => ({
createWidget: useCreateWidgetFromClick(),
draggedArea: useRecoilComponentValue(
draggedArea: useRecoilComponentValueV2(
pageLayoutDraggedAreaComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
editingWidgetId: useRecoilComponentValue(
editingWidgetId: useRecoilComponentValueV2(
pageLayoutEditingWidgetIdComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
@@ -3,8 +3,9 @@ import { useEndPageLayoutDragSelection } from '@/page-layout/hooks/useEndPageLay
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
import { pageLayoutSelectedCellsComponentState } from '@/page-layout/states/pageLayoutSelectedCellsComponentState';
import { calculateGridBoundsFromSelectedCells } from '@/page-layout/utils/calculateGridBoundsFromSelectedCells';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentValueV2';
import { act, renderHook } from '@testing-library/react';
import { createStore } from 'jotai';
import { type ReactNode } from 'react';
import { CommandMenuPages } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
@@ -18,26 +19,29 @@ jest.mock(
);
jest.mock('../../utils/calculateGridBoundsFromSelectedCells');
const createInitializeState =
(initialSelectedCells?: Set<string>, initialDraggedArea?: any) =>
({ set }: { set: any }) => {
if (isDefined(initialSelectedCells)) {
set(
pageLayoutSelectedCellsComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
}),
initialSelectedCells,
);
}
if (initialDraggedArea !== undefined) {
set(
pageLayoutDraggedAreaComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
}),
initialDraggedArea,
);
}
};
const createTestStore = (
initialSelectedCells?: Set<string>,
initialDraggedArea?: any,
) => {
const store = createStore();
if (isDefined(initialSelectedCells)) {
store.set(
pageLayoutSelectedCellsComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
}),
initialSelectedCells,
);
}
if (initialDraggedArea !== undefined) {
store.set(
pageLayoutDraggedAreaComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
}),
initialDraggedArea,
);
}
return store;
};
describe('useEndPageLayoutDragSelection', () => {
const mockNavigatePageLayoutCommandMenu = jest.fn();
@@ -55,28 +59,25 @@ describe('useEndPageLayoutDragSelection', () => {
mockBounds,
);
const store = createTestStore(new Set(['0-0', '0-1', '1-0', '1-1']), null);
const { result } = renderHook(
() => ({
endDragSelection: useEndPageLayoutDragSelection(
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
selectedCells: useRecoilComponentValue(
selectedCells: useRecoilComponentValueV2(
pageLayoutSelectedCellsComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
draggedArea: useRecoilComponentValue(
draggedArea: useRecoilComponentValueV2(
pageLayoutDraggedAreaComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
}),
{
wrapper: ({ children }: { children: ReactNode }) => (
<PageLayoutTestWrapper
initializeState={createInitializeState(
new Set(['0-0', '0-1', '1-0', '1-1']),
null,
)}
>
<PageLayoutTestWrapper store={store}>
{children}
</PageLayoutTestWrapper>
),
@@ -109,25 +110,25 @@ describe('useEndPageLayoutDragSelection', () => {
it('should not navigate when no cells are selected', () => {
(calculateGridBoundsFromSelectedCells as jest.Mock).mockReturnValue(null);
const store = createTestStore(new Set(), null);
const { result } = renderHook(
() => ({
endDragSelection: useEndPageLayoutDragSelection(
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
selectedCells: useRecoilComponentValue(
selectedCells: useRecoilComponentValueV2(
pageLayoutSelectedCellsComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
draggedArea: useRecoilComponentValue(
draggedArea: useRecoilComponentValueV2(
pageLayoutDraggedAreaComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
}),
{
wrapper: ({ children }: { children: ReactNode }) => (
<PageLayoutTestWrapper
initializeState={createInitializeState(new Set(), null)}
>
<PageLayoutTestWrapper store={store}>
{children}
</PageLayoutTestWrapper>
),
@@ -147,28 +148,25 @@ describe('useEndPageLayoutDragSelection', () => {
it('should not navigate when bounds calculation returns null', () => {
(calculateGridBoundsFromSelectedCells as jest.Mock).mockReturnValue(null);
const store = createTestStore(new Set(['invalid-cell']), null);
const { result } = renderHook(
() => ({
endDragSelection: useEndPageLayoutDragSelection(
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
selectedCells: useRecoilComponentValue(
selectedCells: useRecoilComponentValueV2(
pageLayoutSelectedCellsComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
draggedArea: useRecoilComponentValue(
draggedArea: useRecoilComponentValueV2(
pageLayoutDraggedAreaComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
}),
{
wrapper: ({ children }: { children: ReactNode }) => (
<PageLayoutTestWrapper
initializeState={createInitializeState(
new Set(['invalid-cell']),
null,
)}
>
<PageLayoutTestWrapper store={store}>
{children}
</PageLayoutTestWrapper>
),
@@ -206,21 +204,21 @@ describe('useEndPageLayoutDragSelection', () => {
mockBounds,
);
const store = createTestStore(new Set(['0-0']));
const { result } = renderHook(
() => ({
endDragSelection: useEndPageLayoutDragSelection(
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
selectedCells: useRecoilComponentValue(
selectedCells: useRecoilComponentValueV2(
pageLayoutSelectedCellsComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
}),
{
wrapper: ({ children }: { children: ReactNode }) => (
<PageLayoutTestWrapper
initializeState={createInitializeState(new Set(['0-0']))}
>
<PageLayoutTestWrapper store={store}>
{children}
</PageLayoutTestWrapper>
),
@@ -240,21 +238,21 @@ describe('useEndPageLayoutDragSelection', () => {
mockBounds,
);
const store = createTestStore(new Set(['0-0']));
const { result } = renderHook(
() => ({
endDragSelection: useEndPageLayoutDragSelection(
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
selectedCells: useRecoilComponentValue(
selectedCells: useRecoilComponentValueV2(
pageLayoutSelectedCellsComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
}),
{
wrapper: ({ children }: { children: ReactNode }) => (
<PageLayoutTestWrapper
initializeState={createInitializeState(new Set(['0-0']))}
>
<PageLayoutTestWrapper store={store}>
{children}
</PageLayoutTestWrapper>
),
@@ -1,5 +1,5 @@
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentValueV2';
import { act, renderHook } from '@testing-library/react';
import { useSetAtom } from 'jotai';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
@@ -14,7 +14,7 @@ describe('usePageLayoutHandleLayoutChange', () => {
const { result } = renderHook(
() => ({
handler: usePageLayoutHandleLayoutChange(PAGE_LAYOUT_TEST_INSTANCE_ID),
layouts: useRecoilComponentValue(
layouts: useRecoilComponentValueV2(
pageLayoutCurrentLayoutsComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
@@ -57,7 +57,7 @@ describe('usePageLayoutHandleLayoutChange', () => {
const { result } = renderHook(
() => ({
handler: usePageLayoutHandleLayoutChange(PAGE_LAYOUT_TEST_INSTANCE_ID),
layouts: useRecoilComponentValue(
layouts: useRecoilComponentValueV2(
pageLayoutCurrentLayoutsComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
@@ -92,7 +92,7 @@ describe('usePageLayoutHandleLayoutChange', () => {
const { result } = renderHook(
() => ({
handler: usePageLayoutHandleLayoutChange(PAGE_LAYOUT_TEST_INSTANCE_ID),
layouts: useRecoilComponentValue(
layouts: useRecoilComponentValueV2(
pageLayoutCurrentLayoutsComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
@@ -1,5 +1,6 @@
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentValueV2';
import { act, renderHook } from '@testing-library/react';
import { createStore } from 'jotai';
import { type ReactNode } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { pageLayoutSelectedCellsComponentState } from '@/page-layout/states/pageLayoutSelectedCellsComponentState';
@@ -9,38 +10,36 @@ import {
PageLayoutTestWrapper,
} from './PageLayoutTestWrapper';
const createInitializeState =
(initialSelectedCells?: Set<string>) =>
({ set }: { set: any }) => {
if (isDefined(initialSelectedCells)) {
set(
pageLayoutSelectedCellsComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
}),
initialSelectedCells,
);
}
};
const createTestStore = (initialSelectedCells?: Set<string>) => {
const store = createStore();
if (isDefined(initialSelectedCells)) {
store.set(
pageLayoutSelectedCellsComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
}),
initialSelectedCells,
);
}
return store;
};
describe('useStartPageLayoutDragSelection', () => {
it('should clear selected cells when starting drag selection', () => {
const store = createTestStore(new Set(['cell-1', 'cell-2']));
const { result } = renderHook(
() => ({
startDragSelection: useStartPageLayoutDragSelection(
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
selectedCells: useRecoilComponentValue(
selectedCells: useRecoilComponentValueV2(
pageLayoutSelectedCellsComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
}),
{
wrapper: ({ children }: { children: ReactNode }) => (
<PageLayoutTestWrapper
initializeState={createInitializeState(
new Set(['cell-1', 'cell-2']),
)}
>
<PageLayoutTestWrapper store={store}>
{children}
</PageLayoutTestWrapper>
),
@@ -72,21 +71,21 @@ describe('useStartPageLayoutDragSelection', () => {
});
it('should handle multiple calls correctly', () => {
const store = createTestStore(new Set(['cell-1']));
const { result } = renderHook(
() => ({
startDragSelection: useStartPageLayoutDragSelection(
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
selectedCells: useRecoilComponentValue(
selectedCells: useRecoilComponentValueV2(
pageLayoutSelectedCellsComponentState,
PAGE_LAYOUT_TEST_INSTANCE_ID,
),
}),
{
wrapper: ({ children }: { children: ReactNode }) => (
<PageLayoutTestWrapper
initializeState={createInitializeState(new Set(['cell-1']))}
>
<PageLayoutTestWrapper store={store}>
{children}
</PageLayoutTestWrapper>
),
@@ -20,8 +20,8 @@ import { DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID } from '@/page-layout/constants
import { recordPageLayoutFromIdFamilySelector } from '@/page-layout/states/selectors/recordPageLayoutFromIdFamilySelector';
import { type PageLayout } from '@/page-layout/types/PageLayout';
import { transformPageLayout } from '@/page-layout/utils/transformPageLayout';
import { useFamilySelectorValueV2 } from '@/ui/utilities/state/jotai/hooks/useFamilySelectorValueV2';
import { useQuery } from '@apollo/client';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
const getDefaultLayoutById = (layoutId: string): PageLayout => {
@@ -64,8 +64,9 @@ export const useBasePageLayout = (
): PageLayout | undefined => {
const isDefaultLayout = isDefaultLayoutId(pageLayoutId);
const cachedRecordPageLayout = useRecoilValue(
recordPageLayoutFromIdFamilySelector({ pageLayoutId }),
const cachedRecordPageLayout = useFamilySelectorValueV2(
recordPageLayoutFromIdFamilySelector,
{ pageLayoutId },
);
const shouldSkipQuery = isDefaultLayout || isDefined(cachedRecordPageLayout);
@@ -1,7 +1,8 @@
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilCallback } from 'recoil';
import { useRecoilComponentStateCallbackStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentStateCallbackStateV2';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { pageLayoutSelectedCellsComponentState } from '@/page-layout/states/pageLayoutSelectedCellsComponentState';
export const useChangePageLayoutDragSelection = (
@@ -12,25 +13,26 @@ export const useChangePageLayoutDragSelection = (
pageLayoutIdFromProps,
);
const pageLayoutSelectedCellsState = useRecoilComponentCallbackState(
const pageLayoutSelectedCellsState = useRecoilComponentStateCallbackStateV2(
pageLayoutSelectedCellsComponentState,
pageLayoutId,
);
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;
});
},
[pageLayoutSelectedCellsState],
const store = useStore();
const changePageLayoutDragSelection = useCallback(
(cellId: string, selected: boolean) => {
store.set(pageLayoutSelectedCellsState, (prev) => {
const newSet = new Set(prev);
if (selected) {
newSet.add(cellId);
} else {
newSet.delete(cellId);
}
return newSet;
});
},
[pageLayoutSelectedCellsState, store],
);
return { changePageLayoutDragSelection };
@@ -11,9 +11,10 @@ import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTab
import { getUpdatedTabLayouts } from '@/page-layout/utils/getUpdatedTabLayouts';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilComponentStateCallbackStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentStateCallbackStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentValueV2';
import { useRecoilCallback } from 'recoil';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { v4 as uuidv4 } from 'uuid';
import { WidgetType } from '~/generated-metadata/graphql';
@@ -31,93 +32,91 @@ export const useCreatePageLayoutFrontComponentWidget = (
getTabListInstanceIdFromPageLayoutId(pageLayoutId),
);
const pageLayoutCurrentLayoutsState = useRecoilComponentCallbackState(
const pageLayoutCurrentLayoutsState = useRecoilComponentStateCallbackStateV2(
pageLayoutCurrentLayoutsComponentState,
pageLayoutId,
);
const pageLayoutDraggedAreaState = useRecoilComponentCallbackState(
const pageLayoutDraggedAreaState = useRecoilComponentStateCallbackStateV2(
pageLayoutDraggedAreaComponentState,
pageLayoutId,
);
const pageLayoutDraftState = useRecoilComponentCallbackState(
const pageLayoutDraftState = useRecoilComponentStateCallbackStateV2(
pageLayoutDraftComponentState,
pageLayoutId,
);
const createPageLayoutFrontComponentWidget = useRecoilCallback(
({ snapshot, set }) =>
(title: string, frontComponentId: string): PageLayoutWidget => {
const allTabLayouts = snapshot
.getLoadable(pageLayoutCurrentLayoutsState)
.getValue();
const store = useStore();
const pageLayoutDraggedArea = snapshot
.getLoadable(pageLayoutDraggedAreaState)
.getValue();
const createPageLayoutFrontComponentWidget = useCallback(
(title: string, frontComponentId: string): PageLayoutWidget => {
const allTabLayouts = store.get(pageLayoutCurrentLayoutsState);
if (!isDefined(activeTabId)) {
throw new Error(
'A tab must be selected to create a new front component widget',
);
}
const pageLayoutDraggedArea = store.get(pageLayoutDraggedAreaState);
const widgetId = uuidv4();
const frontComponentSize = WIDGET_SIZES[WidgetType.FRONT_COMPONENT]!;
const defaultSize = frontComponentSize.default;
const minimumSize = frontComponentSize.minimum;
const position = getDefaultWidgetPosition(
pageLayoutDraggedArea,
defaultSize,
minimumSize,
if (!isDefined(activeTabId)) {
throw new Error(
'A tab must be selected to create a new front component widget',
);
}
const newWidget = createDefaultFrontComponentWidget(
widgetId,
activeTabId,
title,
frontComponentId,
{
row: position.y,
column: position.x,
rowSpan: position.h,
columnSpan: position.w,
},
);
const widgetId = uuidv4();
const frontComponentSize = WIDGET_SIZES[WidgetType.FRONT_COMPONENT]!;
const defaultSize = frontComponentSize.default;
const minimumSize = frontComponentSize.minimum;
const position = getDefaultWidgetPosition(
pageLayoutDraggedArea,
defaultSize,
minimumSize,
);
const newLayout = {
i: widgetId,
x: position.x,
y: position.y,
w: position.w,
h: position.h,
minW: minimumSize.w,
minH: minimumSize.h,
};
const newWidget = createDefaultFrontComponentWidget(
widgetId,
activeTabId,
title,
frontComponentId,
{
row: position.y,
column: position.x,
rowSpan: position.h,
columnSpan: position.w,
},
);
const updatedLayouts = getUpdatedTabLayouts(
allTabLayouts,
activeTabId,
newLayout,
);
const newLayout = {
i: widgetId,
x: position.x,
y: position.y,
w: position.w,
h: position.h,
minW: minimumSize.w,
minH: minimumSize.h,
};
set(pageLayoutCurrentLayoutsState, updatedLayouts);
const updatedLayouts = getUpdatedTabLayouts(
allTabLayouts,
activeTabId,
newLayout,
);
set(pageLayoutDraftState, (prev) => ({
...prev,
tabs: addWidgetToTab(prev.tabs, activeTabId, newWidget),
}));
store.set(pageLayoutCurrentLayoutsState, updatedLayouts);
set(pageLayoutDraggedAreaState, null);
store.set(pageLayoutDraftState, (prev) => ({
...prev,
tabs: addWidgetToTab(prev.tabs, activeTabId, newWidget),
}));
return newWidget;
},
store.set(pageLayoutDraggedAreaState, null);
return newWidget;
},
[
activeTabId,
pageLayoutCurrentLayoutsState,
pageLayoutDraftState,
pageLayoutDraggedAreaState,
store,
],
);
@@ -15,9 +15,9 @@ import { getWidgetSize } from '@/page-layout/utils/getWidgetSize';
import { getWidgetTitle } from '@/page-layout/utils/getWidgetTitle';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilComponentStateCallbackStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentStateCallbackStateV2';
import { useStore } from 'jotai';
import { useRecoilCallback } from 'recoil';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { v4 as uuidv4 } from 'uuid';
import {
@@ -39,133 +39,124 @@ export const useCreatePageLayoutGraphWidget = (
const tabListInstanceId = getTabListInstanceIdFromPageLayoutId(pageLayoutId);
const pageLayoutDraftState = useRecoilComponentCallbackState(
const pageLayoutDraftState = useRecoilComponentStateCallbackStateV2(
pageLayoutDraftComponentState,
pageLayoutId,
);
const pageLayoutCurrentLayoutsState = useRecoilComponentCallbackState(
const pageLayoutCurrentLayoutsState = useRecoilComponentStateCallbackStateV2(
pageLayoutCurrentLayoutsComponentState,
pageLayoutId,
);
const pageLayoutDraggedAreaState = useRecoilComponentCallbackState(
const pageLayoutDraggedAreaState = useRecoilComponentStateCallbackStateV2(
pageLayoutDraggedAreaComponentState,
pageLayoutId,
);
const createPageLayoutGraphWidget = useRecoilCallback(
({ snapshot, set }) =>
({
fieldSelection,
}: {
fieldSelection?: GraphWidgetFieldSelection;
}): PageLayoutWidget => {
const activeTabId = store.get(
activeTabIdComponentState.atomFamily({
instanceId: tabListInstanceId,
}),
);
const createPageLayoutGraphWidget = useCallback(
({
fieldSelection,
}: {
fieldSelection?: GraphWidgetFieldSelection;
}): PageLayoutWidget => {
const activeTabId = store.get(
activeTabIdComponentState.atomFamily({
instanceId: tabListInstanceId,
}),
);
if (!isDefined(activeTabId)) {
throw new Error(
'A tab must be selected to create a new graph widget',
);
if (!isDefined(activeTabId)) {
throw new Error('A tab must be selected to create a new graph widget');
}
const pageLayoutDraft = store.get(pageLayoutDraftState);
const allTabLayouts = store.get(pageLayoutCurrentLayoutsState);
const pageLayoutDraggedArea = store.get(pageLayoutDraggedAreaState);
const allWidgets = pageLayoutDraft.tabs.flatMap((tab) => tab.widgets);
const existingWidgetCount = allWidgets.filter((widget) => {
if (widget.type !== WidgetType.GRAPH) {
return false;
}
const pageLayoutDraft = snapshot
.getLoadable(pageLayoutDraftState)
.getValue();
if (
!isWidgetConfigurationOfType(
widget.configuration,
'BarChartConfiguration',
)
) {
return false;
}
return widget.configuration.layout === BarChartLayout.VERTICAL;
}).length;
const allTabLayouts = snapshot
.getLoadable(pageLayoutCurrentLayoutsState)
.getValue();
const title = getWidgetTitle(
{
configurationType: WidgetConfigurationType.BAR_CHART,
layout: BarChartLayout.VERTICAL,
},
existingWidgetCount,
);
const widgetId = uuidv4();
const pageLayoutDraggedArea = snapshot
.getLoadable(pageLayoutDraggedAreaState)
.getValue();
const defaultSize = getWidgetSize(
WidgetConfigurationType.BAR_CHART,
'default',
);
const minimumSize = getWidgetSize(
WidgetConfigurationType.BAR_CHART,
'minimum',
);
const position = getDefaultWidgetPosition(
pageLayoutDraggedArea,
defaultSize,
minimumSize,
);
const allWidgets = pageLayoutDraft.tabs.flatMap((tab) => tab.widgets);
const existingWidgetCount = allWidgets.filter((widget) => {
if (widget.type !== WidgetType.GRAPH) {
return false;
}
const newWidget = createDefaultGraphWidget({
id: widgetId,
pageLayoutTabId: activeTabId,
title,
gridPosition: {
row: position.y,
column: position.x,
rowSpan: position.h,
columnSpan: position.w,
},
fieldSelection,
timezone: timeZone,
firstDayOfTheWeek: calendarStartDay,
});
if (
!isWidgetConfigurationOfType(
widget.configuration,
'BarChartConfiguration',
)
) {
return false;
}
return widget.configuration.layout === BarChartLayout.VERTICAL;
}).length;
const newLayout = {
i: widgetId,
x: position.x,
y: position.y,
w: position.w,
h: position.h,
minW: minimumSize.w,
minH: minimumSize.h,
};
const title = getWidgetTitle(
{
configurationType: WidgetConfigurationType.BAR_CHART,
layout: BarChartLayout.VERTICAL,
},
existingWidgetCount,
);
const widgetId = uuidv4();
const updatedLayouts = getUpdatedTabLayouts(
allTabLayouts,
activeTabId,
newLayout,
);
store.set(pageLayoutCurrentLayoutsState, updatedLayouts);
const defaultSize = getWidgetSize(
WidgetConfigurationType.BAR_CHART,
'default',
);
const minimumSize = getWidgetSize(
WidgetConfigurationType.BAR_CHART,
'minimum',
);
const position = getDefaultWidgetPosition(
pageLayoutDraggedArea,
defaultSize,
minimumSize,
);
store.set(pageLayoutDraftState, (prev) => ({
...prev,
tabs: addWidgetToTab(prev.tabs, activeTabId, newWidget),
}));
const newWidget = createDefaultGraphWidget({
id: widgetId,
pageLayoutTabId: activeTabId,
title,
gridPosition: {
row: position.y,
column: position.x,
rowSpan: position.h,
columnSpan: position.w,
},
fieldSelection,
timezone: timeZone,
firstDayOfTheWeek: calendarStartDay,
});
store.set(pageLayoutDraggedAreaState, null);
const newLayout = {
i: widgetId,
x: position.x,
y: position.y,
w: position.w,
h: position.h,
minW: minimumSize.w,
minH: minimumSize.h,
};
const updatedLayouts = getUpdatedTabLayouts(
allTabLayouts,
activeTabId,
newLayout,
);
set(pageLayoutCurrentLayoutsState, updatedLayouts);
set(pageLayoutDraftState, (prev) => ({
...prev,
tabs: addWidgetToTab(prev.tabs, activeTabId, newWidget),
}));
set(pageLayoutDraggedAreaState, null);
return newWidget;
},
return newWidget;
},
[
tabListInstanceId,
pageLayoutCurrentLayoutsState,
@@ -11,9 +11,10 @@ import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTab
import { getUpdatedTabLayouts } from '@/page-layout/utils/getUpdatedTabLayouts';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilComponentStateCallbackStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentStateCallbackStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentValueV2';
import { useRecoilCallback } from 'recoil';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { v4 as uuidv4 } from 'uuid';
import { WidgetType } from '~/generated-metadata/graphql';
@@ -31,93 +32,89 @@ export const useCreatePageLayoutIframeWidget = (
getTabListInstanceIdFromPageLayoutId(pageLayoutId),
);
const pageLayoutCurrentLayoutsState = useRecoilComponentCallbackState(
const pageLayoutCurrentLayoutsState = useRecoilComponentStateCallbackStateV2(
pageLayoutCurrentLayoutsComponentState,
pageLayoutId,
);
const pageLayoutDraggedAreaState = useRecoilComponentCallbackState(
const pageLayoutDraggedAreaState = useRecoilComponentStateCallbackStateV2(
pageLayoutDraggedAreaComponentState,
pageLayoutId,
);
const pageLayoutDraftState = useRecoilComponentCallbackState(
const pageLayoutDraftState = useRecoilComponentStateCallbackStateV2(
pageLayoutDraftComponentState,
pageLayoutId,
);
const createPageLayoutIframeWidget = useRecoilCallback(
({ snapshot, set }) =>
(title: string, url: string | null): PageLayoutWidget => {
const allTabLayouts = snapshot
.getLoadable(pageLayoutCurrentLayoutsState)
.getValue();
const store = useStore();
const pageLayoutDraggedArea = snapshot
.getLoadable(pageLayoutDraggedAreaState)
.getValue();
const createPageLayoutIframeWidget = useCallback(
(title: string, url: string | null): PageLayoutWidget => {
const allTabLayouts = store.get(pageLayoutCurrentLayoutsState);
if (!isDefined(activeTabId)) {
throw new Error(
'A tab must be selected to create a new iframe widget',
);
}
const pageLayoutDraggedArea = store.get(pageLayoutDraggedAreaState);
const widgetId = uuidv4();
const iframeSize = WIDGET_SIZES[WidgetType.IFRAME]!;
const defaultIframeSize = iframeSize.default;
const minimumSize = iframeSize.minimum;
const position = getDefaultWidgetPosition(
pageLayoutDraggedArea,
defaultIframeSize,
minimumSize,
);
if (!isDefined(activeTabId)) {
throw new Error('A tab must be selected to create a new iframe widget');
}
const newWidget = createDefaultIframeWidget(
widgetId,
activeTabId,
title,
url,
{
row: position.y,
column: position.x,
rowSpan: position.h,
columnSpan: position.w,
},
);
const widgetId = uuidv4();
const iframeSize = WIDGET_SIZES[WidgetType.IFRAME]!;
const defaultIframeSize = iframeSize.default;
const minimumSize = iframeSize.minimum;
const position = getDefaultWidgetPosition(
pageLayoutDraggedArea,
defaultIframeSize,
minimumSize,
);
const newLayout = {
i: widgetId,
x: position.x,
y: position.y,
w: position.w,
h: position.h,
minW: minimumSize.w,
minH: minimumSize.h,
};
const newWidget = createDefaultIframeWidget(
widgetId,
activeTabId,
title,
url,
{
row: position.y,
column: position.x,
rowSpan: position.h,
columnSpan: position.w,
},
);
const updatedLayouts = getUpdatedTabLayouts(
allTabLayouts,
activeTabId,
newLayout,
);
const newLayout = {
i: widgetId,
x: position.x,
y: position.y,
w: position.w,
h: position.h,
minW: minimumSize.w,
minH: minimumSize.h,
};
set(pageLayoutCurrentLayoutsState, updatedLayouts);
const updatedLayouts = getUpdatedTabLayouts(
allTabLayouts,
activeTabId,
newLayout,
);
set(pageLayoutDraftState, (prev) => ({
...prev,
tabs: addWidgetToTab(prev.tabs, activeTabId, newWidget),
}));
store.set(pageLayoutCurrentLayoutsState, updatedLayouts);
set(pageLayoutDraggedAreaState, null);
store.set(pageLayoutDraftState, (prev) => ({
...prev,
tabs: addWidgetToTab(prev.tabs, activeTabId, newWidget),
}));
return newWidget;
},
store.set(pageLayoutDraggedAreaState, null);
return newWidget;
},
[
activeTabId,
pageLayoutCurrentLayoutsState,
pageLayoutDraftState,
pageLayoutDraggedAreaState,
store,
],
);
@@ -10,9 +10,9 @@ import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTab
import { getUpdatedTabLayouts } from '@/page-layout/utils/getUpdatedTabLayouts';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilComponentStateCallbackStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentStateCallbackStateV2';
import { useStore } from 'jotai';
import { useRecoilCallback } from 'recoil';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { v4 as uuidv4 } from 'uuid';
import {
@@ -32,94 +32,89 @@ export const useCreatePageLayoutStandaloneRichTextWidget = (
const store = useStore();
const tabListInstanceId = getTabListInstanceIdFromPageLayoutId(pageLayoutId);
const pageLayoutCurrentLayoutsState = useRecoilComponentCallbackState(
const pageLayoutCurrentLayoutsState = useRecoilComponentStateCallbackStateV2(
pageLayoutCurrentLayoutsComponentState,
pageLayoutId,
);
const pageLayoutDraggedAreaState = useRecoilComponentCallbackState(
const pageLayoutDraggedAreaState = useRecoilComponentStateCallbackStateV2(
pageLayoutDraggedAreaComponentState,
pageLayoutId,
);
const pageLayoutDraftState = useRecoilComponentCallbackState(
const pageLayoutDraftState = useRecoilComponentStateCallbackStateV2(
pageLayoutDraftComponentState,
pageLayoutId,
);
const createPageLayoutStandaloneRichTextWidget = useRecoilCallback(
({ snapshot, set }) =>
(body: RichTextV2Body): PageLayoutWidget => {
const activeTabId = store.get(
activeTabIdComponentState.atomFamily({
instanceId: tabListInstanceId,
}),
const createPageLayoutStandaloneRichTextWidget = useCallback(
(body: RichTextV2Body): PageLayoutWidget => {
const activeTabId = store.get(
activeTabIdComponentState.atomFamily({
instanceId: tabListInstanceId,
}),
);
if (!isDefined(activeTabId)) {
throw new Error(
'A tab must be selected to create a new rich text widget',
);
}
if (!isDefined(activeTabId)) {
throw new Error(
'A tab must be selected to create a new rich text widget',
);
}
const allTabLayouts = store.get(pageLayoutCurrentLayoutsState);
const allTabLayouts = snapshot
.getLoadable(pageLayoutCurrentLayoutsState)
.getValue();
const pageLayoutDraggedArea = store.get(pageLayoutDraggedAreaState);
const pageLayoutDraggedArea = snapshot
.getLoadable(pageLayoutDraggedAreaState)
.getValue();
const widgetId = uuidv4();
const richTextSize = WIDGET_SIZES[WidgetType.STANDALONE_RICH_TEXT]!;
const defaultRichTextSize = richTextSize.default;
const minimumSize = richTextSize.minimum;
const position = getDefaultWidgetPosition(
pageLayoutDraggedArea,
defaultRichTextSize,
minimumSize,
);
const widgetId = uuidv4();
const richTextSize = WIDGET_SIZES[WidgetType.STANDALONE_RICH_TEXT]!;
const defaultRichTextSize = richTextSize.default;
const minimumSize = richTextSize.minimum;
const position = getDefaultWidgetPosition(
pageLayoutDraggedArea,
defaultRichTextSize,
minimumSize,
);
const newWidget = createDefaultStandaloneRichTextWidget(
widgetId,
activeTabId,
const newWidget = createDefaultStandaloneRichTextWidget(
widgetId,
activeTabId,
body,
{
row: position.y,
column: position.x,
rowSpan: position.h,
columnSpan: position.w,
},
);
body,
{
row: position.y,
column: position.x,
rowSpan: position.h,
columnSpan: position.w,
},
);
const newLayout = {
i: widgetId,
x: position.x,
y: position.y,
w: position.w,
h: position.h,
minW: minimumSize.w,
minH: minimumSize.h,
};
const newLayout = {
i: widgetId,
x: position.x,
y: position.y,
w: position.w,
h: position.h,
minW: minimumSize.w,
minH: minimumSize.h,
};
const updatedLayouts = getUpdatedTabLayouts(
allTabLayouts,
activeTabId,
newLayout,
);
const updatedLayouts = getUpdatedTabLayouts(
allTabLayouts,
activeTabId,
newLayout,
);
store.set(pageLayoutCurrentLayoutsState, updatedLayouts);
set(pageLayoutCurrentLayoutsState, updatedLayouts);
store.set(pageLayoutDraftState, (prev) => ({
...prev,
tabs: addWidgetToTab(prev.tabs, activeTabId, newWidget),
}));
set(pageLayoutDraftState, (prev) => ({
...prev,
tabs: addWidgetToTab(prev.tabs, activeTabId, newWidget),
}));
store.set(pageLayoutDraggedAreaState, null);
set(pageLayoutDraggedAreaState, null);
return newWidget;
},
return newWidget;
},
[
tabListInstanceId,
pageLayoutCurrentLayoutsState,
@@ -6,9 +6,10 @@ import { getEmptyTabLayout } from '@/page-layout/utils/getEmptyTabLayout';
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilComponentStateCallbackStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentStateCallbackStateV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilComponentStateV2';
import { useRecoilCallback } from 'recoil';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { v4 as uuidv4 } from 'uuid';
export const useCreatePageLayoutTab = (pageLayoutIdFromProps?: string) => {
@@ -17,12 +18,12 @@ export const useCreatePageLayoutTab = (pageLayoutIdFromProps?: string) => {
pageLayoutIdFromProps,
);
const pageLayoutDraftState = useRecoilComponentCallbackState(
const pageLayoutDraftState = useRecoilComponentStateCallbackStateV2(
pageLayoutDraftComponentState,
pageLayoutId,
);
const pageLayoutCurrentLayoutsState = useRecoilComponentCallbackState(
const pageLayoutCurrentLayoutsState = useRecoilComponentStateCallbackStateV2(
pageLayoutCurrentLayoutsComponentState,
pageLayoutId,
);
@@ -33,51 +34,51 @@ export const useCreatePageLayoutTab = (pageLayoutIdFromProps?: string) => {
tabListInstanceId,
);
const createPageLayoutTab = useRecoilCallback(
({ snapshot, set }) =>
(title?: string): string => {
const pageLayoutDraft = snapshot
.getLoadable(pageLayoutDraftState)
.getValue();
const store = useStore();
const newTabId = uuidv4();
const tabsLength = pageLayoutDraft.tabs.length;
const maxPosition =
tabsLength > 0
? Math.max(...pageLayoutDraft.tabs.map((t) => t.position))
: -1;
const newTab: PageLayoutTab = {
id: newTabId,
applicationId: '',
title: title || `Tab ${tabsLength + 1}`,
position: maxPosition + 1,
pageLayoutId: pageLayoutId,
widgets: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
};
const createPageLayoutTab = useCallback(
(title?: string): string => {
const pageLayoutDraft = store.get(pageLayoutDraftState);
const updatedTabs = [...(pageLayoutDraft.tabs || []), newTab];
const newTabId = uuidv4();
const tabsLength = pageLayoutDraft.tabs.length;
const maxPosition =
tabsLength > 0
? Math.max(...pageLayoutDraft.tabs.map((t) => t.position))
: -1;
const newTab: PageLayoutTab = {
id: newTabId,
applicationId: '',
title: title || `Tab ${tabsLength + 1}`,
position: maxPosition + 1,
pageLayoutId: pageLayoutId,
widgets: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
};
set(pageLayoutDraftState, (prev) => ({
...prev,
tabs: updatedTabs,
}));
const updatedTabs = [...(pageLayoutDraft.tabs || []), newTab];
set(pageLayoutCurrentLayoutsState, (prev) =>
getEmptyTabLayout(prev, newTabId),
);
store.set(pageLayoutDraftState, (prev) => ({
...prev,
tabs: updatedTabs,
}));
setActiveTabId(newTabId);
store.set(pageLayoutCurrentLayoutsState, (prev) =>
getEmptyTabLayout(prev, newTabId),
);
return newTabId;
},
setActiveTabId(newTabId);
return newTabId;
},
[
pageLayoutCurrentLayoutsState,
pageLayoutDraftState,
pageLayoutId,
setActiveTabId,
store,
],
);
@@ -2,39 +2,42 @@ import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layo
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { parseCellIdToCoordinates } from '@/page-layout/utils/parseCellIdToCoordinates';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilCallback } from 'recoil';
import { useRecoilComponentStateCallbackStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentStateCallbackStateV2';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { CommandMenuPages } from 'twenty-shared/types';
export const useCreateWidgetFromClick = () => {
const pageLayoutDraggedAreaState = useRecoilComponentCallbackState(
const pageLayoutDraggedAreaState = useRecoilComponentStateCallbackStateV2(
pageLayoutDraggedAreaComponentState,
);
const pageLayoutEditingWidgetIdState = useRecoilComponentCallbackState(
const pageLayoutEditingWidgetIdState = useRecoilComponentStateCallbackStateV2(
pageLayoutEditingWidgetIdComponentState,
);
const { navigatePageLayoutCommandMenu } = useNavigatePageLayoutCommandMenu();
const createWidgetFromClick = useRecoilCallback(
({ set }) =>
(cellId: string) => {
const { col, row } = parseCellIdToCoordinates(cellId);
const bounds = { x: col, y: row, w: 1, h: 1 };
const store = useStore();
set(pageLayoutDraggedAreaState, bounds);
set(pageLayoutEditingWidgetIdState, null);
const createWidgetFromClick = useCallback(
(cellId: string) => {
const { col, row } = parseCellIdToCoordinates(cellId);
const bounds = { x: col, y: row, w: 1, h: 1 };
navigatePageLayoutCommandMenu({
commandMenuPage: CommandMenuPages.PageLayoutWidgetTypeSelect,
resetNavigationStack: true,
});
},
store.set(pageLayoutDraggedAreaState, bounds);
store.set(pageLayoutEditingWidgetIdState, null);
navigatePageLayoutCommandMenu({
commandMenuPage: CommandMenuPages.PageLayoutWidgetTypeSelect,
resetNavigationStack: true,
});
},
[
navigatePageLayoutCommandMenu,
pageLayoutDraggedAreaState,
pageLayoutEditingWidgetIdState,
store,
],
);
@@ -1,18 +1,18 @@
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentValueV2';
export const useCurrentPageLayout = () => {
const pageLayoutPersisted = useRecoilComponentValue(
const pageLayoutPersisted = useRecoilComponentValueV2(
pageLayoutPersistedComponentState,
);
const pageLayoutDraft = useRecoilComponentValue(
const pageLayoutDraft = useRecoilComponentValueV2(
pageLayoutDraftComponentState,
);
const isPageLayoutInEditMode = useRecoilComponentValue(
const isPageLayoutInEditMode = useRecoilComponentValueV2(
isPageLayoutInEditModeComponentState,
);
@@ -6,9 +6,9 @@ import { removeTabLayouts } from '@/page-layout/utils/removeTabLayouts';
import { sortTabsByPosition } from '@/page-layout/utils/sortTabsByPosition';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilComponentStateCallbackStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentStateCallbackStateV2';
import { useStore } from 'jotai';
import { useRecoilCallback } from 'recoil';
import { useCallback } from 'react';
export const useDeletePageLayoutTab = (pageLayoutIdFromProps?: string) => {
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
@@ -16,12 +16,12 @@ export const useDeletePageLayoutTab = (pageLayoutIdFromProps?: string) => {
pageLayoutIdFromProps,
);
const pageLayoutDraftState = useRecoilComponentCallbackState(
const pageLayoutDraftState = useRecoilComponentStateCallbackStateV2(
pageLayoutDraftComponentState,
pageLayoutId,
);
const pageLayoutCurrentLayoutsState = useRecoilComponentCallbackState(
const pageLayoutCurrentLayoutsState = useRecoilComponentStateCallbackStateV2(
pageLayoutCurrentLayoutsComponentState,
pageLayoutId,
);
@@ -33,36 +33,33 @@ export const useDeletePageLayoutTab = (pageLayoutIdFromProps?: string) => {
instanceId: tabListInstanceId,
});
const deleteTab = useRecoilCallback(
({ set, snapshot }) =>
(tabId: string) => {
const draft = snapshot.getLoadable(pageLayoutDraftState).getValue();
if (draft.tabs.length <= 1) {
return;
}
const deleteTab = useCallback(
(tabId: string) => {
const draft = store.get(pageLayoutDraftState);
if (draft.tabs.length <= 1) {
return;
}
const sorted = sortTabsByPosition(draft.tabs);
const index = sorted.findIndex((t) => t.id === tabId);
const sorted = sortTabsByPosition(draft.tabs);
const index = sorted.findIndex((t) => t.id === tabId);
const activeTabId = store.get(activeTabIdAtom);
const activeTabId = store.get(activeTabIdAtom);
const allLayouts = snapshot
.getLoadable(pageLayoutCurrentLayoutsState)
.getValue();
const updatedLayouts = removeTabLayouts(allLayouts, tabId);
set(pageLayoutCurrentLayoutsState, updatedLayouts);
const allLayouts = store.get(pageLayoutCurrentLayoutsState);
const updatedLayouts = removeTabLayouts(allLayouts, tabId);
store.set(pageLayoutCurrentLayoutsState, updatedLayouts);
set(pageLayoutDraftState, (prev) => ({
...prev,
tabs: prev.tabs.filter((t) => t.id !== tabId),
}));
store.set(pageLayoutDraftState, (prev) => ({
...prev,
tabs: prev.tabs.filter((t) => t.id !== tabId),
}));
if (activeTabId === tabId) {
const neighbor = index > 0 ? sorted[index - 1] : sorted[index + 1];
const nextActiveId = neighbor?.id ?? null;
store.set(activeTabIdAtom, nextActiveId);
}
},
if (activeTabId === tabId) {
const neighbor = index > 0 ? sorted[index - 1] : sorted[index + 1];
const nextActiveId = neighbor?.id ?? null;
store.set(activeTabIdAtom, nextActiveId);
}
},
[
pageLayoutCurrentLayoutsState,
pageLayoutDraftState,
@@ -1,8 +1,9 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilCallback } from 'recoil';
import { useRecoilComponentStateCallbackStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentStateCallbackStateV2';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
@@ -15,50 +16,52 @@ export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
pageLayoutIdFromProps,
);
const pageLayoutDraftState = useRecoilComponentCallbackState(
const pageLayoutDraftState = useRecoilComponentStateCallbackStateV2(
pageLayoutDraftComponentState,
pageLayoutId,
);
const pageLayoutCurrentLayoutsState = useRecoilComponentCallbackState(
const pageLayoutCurrentLayoutsState = useRecoilComponentStateCallbackStateV2(
pageLayoutCurrentLayoutsComponentState,
pageLayoutId,
);
const { closeCommandMenu } = useCommandMenu();
const deletePageLayoutWidget = useRecoilCallback(
({ snapshot, set }) =>
(widgetId: string) => {
closeCommandMenu();
const store = useStore();
const pageLayoutDraft = snapshot
.getLoadable(pageLayoutDraftState)
.getValue();
const allTabLayouts = snapshot
.getLoadable(pageLayoutCurrentLayoutsState)
.getValue();
const deletePageLayoutWidget = useCallback(
(widgetId: string) => {
closeCommandMenu();
const tabWithWidget = pageLayoutDraft.tabs.find((tab) =>
tab.widgets.some((w) => w.id === widgetId),
const pageLayoutDraft = store.get(pageLayoutDraftState);
const allTabLayouts = store.get(pageLayoutCurrentLayoutsState);
const tabWithWidget = pageLayoutDraft.tabs.find((tab) =>
tab.widgets.some((w) => w.id === widgetId),
);
const tabId = tabWithWidget?.id;
if (isDefined(tabId)) {
const updatedLayouts = removeWidgetLayoutFromTab(
allTabLayouts,
tabId,
widgetId,
);
const tabId = tabWithWidget?.id;
store.set(pageLayoutCurrentLayoutsState, updatedLayouts);
if (isDefined(tabId)) {
const updatedLayouts = removeWidgetLayoutFromTab(
allTabLayouts,
tabId,
widgetId,
);
set(pageLayoutCurrentLayoutsState, updatedLayouts);
set(pageLayoutDraftState, (prev) => ({
...prev,
tabs: removeWidgetFromTab(prev.tabs, tabId, widgetId),
}));
}
},
[closeCommandMenu, pageLayoutCurrentLayoutsState, pageLayoutDraftState],
store.set(pageLayoutDraftState, (prev) => ({
...prev,
tabs: removeWidgetFromTab(prev.tabs, tabId, widgetId),
}));
}
},
[
closeCommandMenu,
pageLayoutCurrentLayoutsState,
pageLayoutDraftState,
store,
],
);
return { deletePageLayoutWidget };
@@ -12,10 +12,10 @@ import { sortTabsByPosition } from '@/page-layout/utils/sortTabsByPosition';
import { calculateNewPosition } from '@/ui/layout/draggable-list/utils/calculateNewPosition';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { useRecoilComponentStateCallbackStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentStateCallbackStateV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilComponentStateV2';
import { useRecoilCallback } from 'recoil';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { appendCopySuffix, isDefined } from 'twenty-shared/utils';
import { v4 as uuidv4 } from 'uuid';
@@ -25,23 +25,25 @@ export const useDuplicatePageLayoutTab = (pageLayoutIdFromProps?: string) => {
pageLayoutIdFromProps,
);
const pageLayoutDraftState = useRecoilComponentCallbackState(
const pageLayoutDraftAtom = useRecoilComponentStateCallbackStateV2(
pageLayoutDraftComponentState,
pageLayoutId,
);
const pageLayoutCurrentLayoutsState = useRecoilComponentCallbackState(
const pageLayoutCurrentLayoutsAtom = useRecoilComponentStateCallbackStateV2(
pageLayoutCurrentLayoutsComponentState,
pageLayoutId,
);
const store = useStore();
const tabListInstanceId = getTabListInstanceIdFromPageLayoutId(pageLayoutId);
const setActiveTabId = useSetRecoilComponentStateV2(
activeTabIdComponentState,
tabListInstanceId,
);
const setTabSettingsOpenTabId = useSetRecoilComponentState(
const setTabSettingsOpenTabId = useSetRecoilComponentStateV2(
pageLayoutTabSettingsOpenTabIdComponentState,
pageLayoutId,
);
@@ -50,103 +52,100 @@ export const useDuplicatePageLayoutTab = (pageLayoutIdFromProps?: string) => {
const { closeCommandMenu } = useCommandMenu();
const duplicateTab = useRecoilCallback(
({ snapshot, set }) =>
(tabId: string): string => {
const pageLayoutDraft = snapshot
.getLoadable(pageLayoutDraftState)
.getValue();
const duplicateTab = useCallback(
(tabId: string): string => {
const pageLayoutDraft = store.get(pageLayoutDraftAtom);
const allTabLayouts = snapshot
.getLoadable(pageLayoutCurrentLayoutsState)
.getValue();
const allTabLayouts = store.get(pageLayoutCurrentLayoutsAtom);
const sourceTab = pageLayoutDraft.tabs.find((t) => t.id === tabId);
const sourceTab = pageLayoutDraft.tabs.find((t) => t.id === tabId);
if (!isDefined(sourceTab)) {
throw new Error(`Tab with id ${tabId} not found`);
}
if (!isDefined(sourceTab)) {
throw new Error(`Tab with id ${tabId} not found`);
}
const newTabId = uuidv4();
const widgetOldIdNewIdMap = new Map<string, string>();
const newTabId = uuidv4();
const widgetOldIdNewIdMap = new Map<string, string>();
const clonedWidgets = sourceTab.widgets.map((widget) => {
const newWidgetId = uuidv4();
widgetOldIdNewIdMap.set(widget.id, newWidgetId);
const clonedWidgets = sourceTab.widgets.map((widget) => {
const newWidgetId = uuidv4();
widgetOldIdNewIdMap.set(widget.id, newWidgetId);
return {
...widget,
id: newWidgetId,
pageLayoutTabId: newTabId,
...generateDuplicatedTimestamps(),
};
});
const sortedTabs = sortTabsByPosition(pageLayoutDraft.tabs);
const sourceIndex = sortedTabs.findIndex((t) => t.id === tabId);
const newTabPosition = calculateNewPosition({
items: sortedTabs,
destinationIndex: sourceIndex + 1,
sourceIndex,
});
const newTab: PageLayoutTab = {
...sourceTab,
id: newTabId,
title: appendCopySuffix(sourceTab.title),
position: newTabPosition,
widgets: clonedWidgets,
return {
...widget,
id: newWidgetId,
pageLayoutTabId: newTabId,
...generateDuplicatedTimestamps(),
};
});
const sourceLayouts = allTabLayouts[tabId] ?? {
desktop: [],
mobile: [],
};
const sortedTabs = sortTabsByPosition(pageLayoutDraft.tabs);
const sourceIndex = sortedTabs.findIndex((t) => t.id === tabId);
const newLayouts = {
desktop: sourceLayouts.desktop.map((layout) => ({
...layout,
i: widgetOldIdNewIdMap.get(layout.i) || layout.i,
})),
mobile: sourceLayouts.mobile.map((layout) => ({
...layout,
i: widgetOldIdNewIdMap.get(layout.i) || layout.i,
})),
};
const newTabPosition = calculateNewPosition({
items: sortedTabs,
destinationIndex: sourceIndex + 1,
sourceIndex,
});
set(pageLayoutCurrentLayoutsState, {
...allTabLayouts,
[newTabId]: newLayouts,
});
const newTab: PageLayoutTab = {
...sourceTab,
id: newTabId,
title: appendCopySuffix(sourceTab.title),
position: newTabPosition,
widgets: clonedWidgets,
...generateDuplicatedTimestamps(),
};
set(pageLayoutDraftState, (prev) => ({
...prev,
tabs: [...prev.tabs, newTab],
}));
const sourceLayouts = allTabLayouts[tabId] ?? {
desktop: [],
mobile: [],
};
closeCommandMenu();
const newLayouts = {
desktop: sourceLayouts.desktop.map((layout) => ({
...layout,
i: widgetOldIdNewIdMap.get(layout.i) || layout.i,
})),
mobile: sourceLayouts.mobile.map((layout) => ({
...layout,
i: widgetOldIdNewIdMap.get(layout.i) || layout.i,
})),
};
setActiveTabId(newTabId);
store.set(pageLayoutCurrentLayoutsAtom, {
...allTabLayouts,
[newTabId]: newLayouts,
});
setTabSettingsOpenTabId(newTabId);
const prev = store.get(pageLayoutDraftAtom);
store.set(pageLayoutDraftAtom, {
...prev,
tabs: [...prev.tabs, newTab],
});
navigatePageLayoutCommandMenu({
commandMenuPage: CommandMenuPages.PageLayoutTabSettings,
pageTitle: newTab.title,
focusTitleInput: true,
});
closeCommandMenu();
return newTabId;
},
setActiveTabId(newTabId);
setTabSettingsOpenTabId(newTabId);
navigatePageLayoutCommandMenu({
commandMenuPage: CommandMenuPages.PageLayoutTabSettings,
pageTitle: newTab.title,
focusTitleInput: true,
});
return newTabId;
},
[
closeCommandMenu,
navigatePageLayoutCommandMenu,
pageLayoutCurrentLayoutsState,
pageLayoutDraftState,
pageLayoutCurrentLayoutsAtom,
pageLayoutDraftAtom,
setActiveTabId,
setTabSettingsOpenTabId,
store,
],
);
@@ -9,9 +9,10 @@ import { getScrollWrapperInstanceIdFromPageLayoutId } from '@/page-layout/utils/
import { getUpdatedTabLayouts } from '@/page-layout/utils/getUpdatedTabLayouts';
import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { useRecoilCallback } from 'recoil';
import { useRecoilComponentStateCallbackStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentStateCallbackStateV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilComponentStateV2';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { appendCopySuffix, isDefined } from 'twenty-shared/utils';
import { v4 as uuidv4 } from 'uuid';
@@ -23,17 +24,17 @@ export const useDuplicatePageLayoutWidget = (
pageLayoutIdFromProps,
);
const pageLayoutDraftState = useRecoilComponentCallbackState(
const pageLayoutDraftState = useRecoilComponentStateCallbackStateV2(
pageLayoutDraftComponentState,
pageLayoutId,
);
const pageLayoutCurrentLayoutsState = useRecoilComponentCallbackState(
const pageLayoutCurrentLayoutsState = useRecoilComponentStateCallbackStateV2(
pageLayoutCurrentLayoutsComponentState,
pageLayoutId,
);
const setEditingWidgetId = useSetRecoilComponentState(
const setEditingWidgetId = useSetRecoilComponentStateV2(
pageLayoutEditingWidgetIdComponentState,
pageLayoutId,
);
@@ -42,107 +43,105 @@ export const useDuplicatePageLayoutWidget = (
getScrollWrapperInstanceIdFromPageLayoutId(pageLayoutId),
);
const duplicateWidget = useRecoilCallback(
({ snapshot, set }) =>
(widgetId: string): string => {
const pageLayoutDraft = snapshot
.getLoadable(pageLayoutDraftState)
.getValue();
const store = useStore();
const allTabLayouts = snapshot
.getLoadable(pageLayoutCurrentLayoutsState)
.getValue();
const duplicateWidget = useCallback(
(widgetId: string): string => {
const pageLayoutDraft = store.get(pageLayoutDraftState);
const sourceWidget = pageLayoutDraft.tabs
.flatMap((tab) => tab.widgets)
.find((widget) => widget.id === widgetId);
const allTabLayouts = store.get(pageLayoutCurrentLayoutsState);
if (!isDefined(sourceWidget)) {
throw new Error(`Widget with id ${widgetId} not found`);
}
const sourceWidget = pageLayoutDraft.tabs
.flatMap((tab) => tab.widgets)
.find((widget) => widget.id === widgetId);
const sourceTab = pageLayoutDraft.tabs.find(
(tab) => tab.id === sourceWidget.pageLayoutTabId,
if (!isDefined(sourceWidget)) {
throw new Error(`Widget with id ${widgetId} not found`);
}
const sourceTab = pageLayoutDraft.tabs.find(
(tab) => tab.id === sourceWidget.pageLayoutTabId,
);
if (!isDefined(sourceTab)) {
throw new Error(
`Tab with id ${sourceWidget.pageLayoutTabId} not found`,
);
}
if (!isDefined(sourceTab)) {
throw new Error(
`Tab with id ${sourceWidget.pageLayoutTabId} not found`,
const newWidgetId = uuidv4();
const clonedWidget: PageLayoutWidget = {
...sourceWidget,
id: newWidgetId,
title: appendCopySuffix(sourceWidget.title),
...generateDuplicatedTimestamps(),
};
const currentTabLayouts = allTabLayouts[sourceTab.id] || {
desktop: [],
mobile: [],
};
const sourceLayout = currentTabLayouts.desktop.find(
(layout) => layout.i === widgetId,
);
if (!isDefined(sourceLayout)) {
throw new Error(`Layout for widget ${widgetId} not found`);
}
const maxY = currentTabLayouts.desktop.reduce(
(max, layout) => Math.max(max, layout.y + layout.h),
0,
);
const newLayout = {
...sourceLayout,
i: newWidgetId,
y: maxY,
};
const updatedLayouts = getUpdatedTabLayouts(
allTabLayouts,
sourceTab.id,
newLayout,
);
store.set(pageLayoutCurrentLayoutsState, updatedLayouts);
store.set(pageLayoutDraftState, (prev) => ({
...prev,
tabs: addWidgetToTab(prev.tabs, sourceTab.id, clonedWidget),
}));
setEditingWidgetId(newWidgetId);
const { scrollWrapperElement } = getScrollWrapperElement();
if (isDefined(scrollWrapperElement)) {
requestAnimationFrame(() => {
const widgetElement = scrollWrapperElement.querySelector(
`[data-widget-id="${newWidgetId}"]`,
);
}
const newWidgetId = uuidv4();
if (isDefined(widgetElement)) {
widgetElement.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
});
}
});
}
const clonedWidget: PageLayoutWidget = {
...sourceWidget,
id: newWidgetId,
title: appendCopySuffix(sourceWidget.title),
...generateDuplicatedTimestamps(),
};
const currentTabLayouts = allTabLayouts[sourceTab.id] || {
desktop: [],
mobile: [],
};
const sourceLayout = currentTabLayouts.desktop.find(
(layout) => layout.i === widgetId,
);
if (!isDefined(sourceLayout)) {
throw new Error(`Layout for widget ${widgetId} not found`);
}
const maxY = currentTabLayouts.desktop.reduce(
(max, layout) => Math.max(max, layout.y + layout.h),
0,
);
const newLayout = {
...sourceLayout,
i: newWidgetId,
y: maxY,
};
const updatedLayouts = getUpdatedTabLayouts(
allTabLayouts,
sourceTab.id,
newLayout,
);
set(pageLayoutCurrentLayoutsState, updatedLayouts);
set(pageLayoutDraftState, (prev) => ({
...prev,
tabs: addWidgetToTab(prev.tabs, sourceTab.id, clonedWidget),
}));
setEditingWidgetId(newWidgetId);
const { scrollWrapperElement } = getScrollWrapperElement();
if (isDefined(scrollWrapperElement)) {
requestAnimationFrame(() => {
const widgetElement = scrollWrapperElement.querySelector(
`[data-widget-id="${newWidgetId}"]`,
);
if (isDefined(widgetElement)) {
widgetElement.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
});
}
});
}
return newWidgetId;
},
return newWidgetId;
},
[
pageLayoutCurrentLayoutsState,
pageLayoutDraftState,
setEditingWidgetId,
getScrollWrapperElement,
store,
],
);
@@ -1,15 +1,15 @@
import { useCallback } from 'react';
import { useSetRecoilState } from 'recoil';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilComponentStateV2';
import { CommandMenuPages } from 'twenty-shared/types';
import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { t } from '@lingui/core/macro';
import { WidgetType } from '~/generated-metadata/graphql';
@@ -19,14 +19,14 @@ export const useEditPageLayoutWidget = (pageLayoutIdFromProps?: string) => {
pageLayoutIdFromProps,
);
const setPageLayoutEditingWidgetId = useSetRecoilComponentState(
const setPageLayoutEditingWidgetId = useSetRecoilComponentStateV2(
pageLayoutEditingWidgetIdComponentState,
pageLayoutId,
);
const { navigatePageLayoutCommandMenu } = useNavigatePageLayoutCommandMenu();
const { closeCommandMenu } = useCommandMenu();
const setCommandMenuPage = useSetRecoilState(commandMenuPageState);
const setCommandMenuPage = useSetRecoilStateV2(commandMenuPageState);
const handleEditWidget = useCallback(
({
@@ -5,8 +5,9 @@ import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pa
import { pageLayoutSelectedCellsComponentState } from '@/page-layout/states/pageLayoutSelectedCellsComponentState';
import { calculateGridBoundsFromSelectedCells } from '@/page-layout/utils/calculateGridBoundsFromSelectedCells';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilCallback } from 'recoil';
import { useRecoilComponentStateCallbackStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentStateCallbackStateV2';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { CommandMenuPages } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
@@ -18,54 +19,51 @@ export const useEndPageLayoutDragSelection = (
pageLayoutIdFromProps,
);
const pageLayoutSelectedCellsState = useRecoilComponentCallbackState(
const pageLayoutSelectedCellsState = useRecoilComponentStateCallbackStateV2(
pageLayoutSelectedCellsComponentState,
pageLayoutId,
);
const pageLayoutDraggedAreaState = useRecoilComponentCallbackState(
const pageLayoutDraggedAreaState = useRecoilComponentStateCallbackStateV2(
pageLayoutDraggedAreaComponentState,
pageLayoutId,
);
const pageLayoutEditingWidgetIdState = useRecoilComponentCallbackState(
const pageLayoutEditingWidgetIdState = useRecoilComponentStateCallbackStateV2(
pageLayoutEditingWidgetIdComponentState,
pageLayoutId,
);
const { navigatePageLayoutCommandMenu } = useNavigatePageLayoutCommandMenu();
const endPageLayoutDragSelection = useRecoilCallback(
({ snapshot, set }) =>
() => {
const pageLayoutSelectedCells = snapshot
.getLoadable(pageLayoutSelectedCellsState)
.getValue();
const store = useStore();
if (pageLayoutSelectedCells.size > 0) {
const draggedBounds = calculateGridBoundsFromSelectedCells(
Array.from(pageLayoutSelectedCells),
);
const endPageLayoutDragSelection = useCallback(() => {
const pageLayoutSelectedCells = store.get(pageLayoutSelectedCellsState);
if (isDefined(draggedBounds)) {
set(pageLayoutDraggedAreaState, draggedBounds);
set(pageLayoutEditingWidgetIdState, null);
if (pageLayoutSelectedCells.size > 0) {
const draggedBounds = calculateGridBoundsFromSelectedCells(
Array.from(pageLayoutSelectedCells),
);
navigatePageLayoutCommandMenu({
commandMenuPage: CommandMenuPages.PageLayoutWidgetTypeSelect,
resetNavigationStack: true,
});
}
}
if (isDefined(draggedBounds)) {
store.set(pageLayoutDraggedAreaState, draggedBounds);
store.set(pageLayoutEditingWidgetIdState, null);
set(pageLayoutSelectedCellsState, new Set());
},
[
navigatePageLayoutCommandMenu,
pageLayoutDraggedAreaState,
pageLayoutEditingWidgetIdState,
pageLayoutSelectedCellsState,
],
);
navigatePageLayoutCommandMenu({
commandMenuPage: CommandMenuPages.PageLayoutWidgetTypeSelect,
resetNavigationStack: true,
});
}
}
store.set(pageLayoutSelectedCellsState, new Set());
}, [
navigatePageLayoutCommandMenu,
pageLayoutDraggedAreaState,
pageLayoutEditingWidgetIdState,
pageLayoutSelectedCellsState,
store,
]);
return { endPageLayoutDragSelection };
};
@@ -3,8 +3,9 @@ import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDr
import { sortTabsByPosition } from '@/page-layout/utils/sortTabsByPosition';
import { calculateNewPosition } from '@/ui/layout/draggable-list/utils/calculateNewPosition';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilCallback } from 'recoil';
import { useRecoilComponentStateCallbackStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentStateCallbackStateV2';
import { useStore } from 'jotai';
import { useCallback } from 'react';
export const useMovePageLayoutTab = (pageLayoutIdFromProps?: string) => {
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
@@ -12,67 +13,67 @@ export const useMovePageLayoutTab = (pageLayoutIdFromProps?: string) => {
pageLayoutIdFromProps,
);
const pageLayoutDraftState = useRecoilComponentCallbackState(
const pageLayoutDraftState = useRecoilComponentStateCallbackStateV2(
pageLayoutDraftComponentState,
pageLayoutId,
);
const moveLeft = useRecoilCallback(
({ set }) =>
(tabId: string) => {
set(pageLayoutDraftState, (prev) => {
const sorted = sortTabsByPosition(prev.tabs);
const index = sorted.findIndex((t) => t.id === tabId);
if (index <= 0) return prev;
const store = useStore();
const items = sorted.filter((t) => t.id !== tabId);
const destinationIndex = index - 1;
const sourceIndex = index;
const moveLeft = useCallback(
(tabId: string) => {
store.set(pageLayoutDraftState, (prev) => {
const sorted = sortTabsByPosition(prev.tabs);
const index = sorted.findIndex((t) => t.id === tabId);
if (index <= 0) return prev;
const newPosition = calculateNewPosition({
destinationIndex,
sourceIndex,
items,
});
const items = sorted.filter((t) => t.id !== tabId);
const destinationIndex = index - 1;
const sourceIndex = index;
return {
...prev,
tabs: prev.tabs.map((t) =>
t.id === tabId ? { ...t, position: newPosition } : t,
),
};
const newPosition = calculateNewPosition({
destinationIndex,
sourceIndex,
items,
});
},
[pageLayoutDraftState],
return {
...prev,
tabs: prev.tabs.map((t) =>
t.id === tabId ? { ...t, position: newPosition } : t,
),
};
});
},
[pageLayoutDraftState, store],
);
const moveRight = useRecoilCallback(
({ set }) =>
(tabId: string) => {
set(pageLayoutDraftState, (prev) => {
const sorted = sortTabsByPosition(prev.tabs);
const index = sorted.findIndex((t) => t.id === tabId);
if (index < 0 || index >= sorted.length - 1) return prev;
const moveRight = useCallback(
(tabId: string) => {
store.set(pageLayoutDraftState, (prev) => {
const sorted = sortTabsByPosition(prev.tabs);
const index = sorted.findIndex((t) => t.id === tabId);
if (index < 0 || index >= sorted.length - 1) return prev;
const items = sorted.filter((t) => t.id !== tabId);
const destinationIndex = index + 1;
const sourceIndex = index;
const items = sorted.filter((t) => t.id !== tabId);
const destinationIndex = index + 1;
const sourceIndex = index;
const newPosition = calculateNewPosition({
destinationIndex,
sourceIndex,
items,
});
return {
...prev,
tabs: prev.tabs.map((t) =>
t.id === tabId ? { ...t, position: newPosition } : t,
),
};
const newPosition = calculateNewPosition({
destinationIndex,
sourceIndex,
items,
});
},
[pageLayoutDraftState],
return {
...prev,
tabs: prev.tabs.map((t) =>
t.id === tabId ? { ...t, position: newPosition } : t,
),
};
});
},
[pageLayoutDraftState, store],
);
return { moveLeft, moveRight };
@@ -1,7 +1,7 @@
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentValueV2';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
@@ -12,11 +12,11 @@ export const usePageLayoutDraftState = (pageLayoutIdFromProps?: string) => {
pageLayoutIdFromProps,
);
const [pageLayoutDraft, setPageLayoutDraft] = useRecoilComponentState(
const [pageLayoutDraft, setPageLayoutDraft] = useRecoilComponentStateV2(
pageLayoutDraftComponentState,
pageLayoutId,
);
const pageLayoutPersisted = useRecoilComponentValue(
const pageLayoutPersisted = useRecoilComponentValueV2(
pageLayoutPersistedComponentState,
pageLayoutId,
);
@@ -2,10 +2,10 @@ import { PageLayoutComponentInstanceContext } from '@/page-layout/states/context
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilComponentStateCallbackStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentStateCallbackStateV2';
import { useStore } from 'jotai';
import { type Layout, type Layouts } from 'react-grid-layout';
import { useRecoilCallback } from 'recoil';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
@@ -22,69 +22,64 @@ export const usePageLayoutHandleLayoutChange = (
const store = useStore();
const tabListInstanceId = getTabListInstanceIdFromPageLayoutId(pageLayoutId);
const pageLayoutCurrentLayoutsState = useRecoilComponentCallbackState(
const pageLayoutCurrentLayoutsState = useRecoilComponentStateCallbackStateV2(
pageLayoutCurrentLayoutsComponentState,
pageLayoutId,
);
const pageLayoutDraftState = useRecoilComponentCallbackState(
const pageLayoutDraftState = useRecoilComponentStateCallbackStateV2(
pageLayoutDraftComponentState,
pageLayoutId,
);
const handleLayoutChange = useRecoilCallback(
({ snapshot, set }) =>
(_: Layout[], allLayouts: Layouts) => {
const activeTabId = store.get(
activeTabIdComponentState.atomFamily({
instanceId: tabListInstanceId,
const handleLayoutChange = useCallback(
(_: Layout[], allLayouts: Layouts) => {
const activeTabId = store.get(
activeTabIdComponentState.atomFamily({
instanceId: tabListInstanceId,
}),
);
if (!isDefined(activeTabId)) return;
const currentTabLayouts = store.get(pageLayoutCurrentLayoutsState);
store.set(pageLayoutCurrentLayoutsState, {
...currentTabLayouts,
[activeTabId]: structuredClone(allLayouts),
});
const pageLayoutDraft = store.get(pageLayoutDraftState);
const currentTab = pageLayoutDraft.tabs.find(
(tab) => tab.id === activeTabId,
);
if (!currentTab) return;
const updatedWidgets = convertLayoutsToWidgets(
currentTab.widgets,
allLayouts,
);
if (isDefined(activeTabId)) {
store.set(pageLayoutDraftState, (prev) => ({
...prev,
tabs: prev.tabs.map((tab) => {
if (tab.id === activeTabId) {
const tabWidgets = updatedWidgets.filter(
(w) => w.pageLayoutTabId === activeTabId,
);
return {
...tab,
widgets: tabWidgets,
};
}
return tab;
}),
);
if (!isDefined(activeTabId)) return;
const currentTabLayouts = snapshot
.getLoadable(pageLayoutCurrentLayoutsState)
.getValue();
set(pageLayoutCurrentLayoutsState, {
...currentTabLayouts,
[activeTabId]: structuredClone(allLayouts),
});
const pageLayoutDraft = snapshot
.getLoadable(pageLayoutDraftState)
.getValue();
const currentTab = pageLayoutDraft.tabs.find(
(tab) => tab.id === activeTabId,
);
if (!currentTab) return;
const updatedWidgets = convertLayoutsToWidgets(
currentTab.widgets,
allLayouts,
);
if (isDefined(activeTabId)) {
set(pageLayoutDraftState, (prev) => ({
...prev,
tabs: prev.tabs.map((tab) => {
if (tab.id === activeTabId) {
const tabWidgets = updatedWidgets.filter(
(w) => w.pageLayoutTabId === activeTabId,
);
return {
...tab,
widgets: tabWidgets,
};
}
return tab;
}),
}));
}
},
}));
}
},
[
tabListInstanceId,
pageLayoutCurrentLayoutsState,
@@ -1,5 +1,3 @@
import { useRecoilValue } from 'recoil';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
@@ -7,6 +5,7 @@ import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { recordPageLayoutByObjectMetadataIdFamilySelector } from '@/page-layout/states/selectors/recordPageLayoutByObjectMetadataIdFamilySelector';
import { getDefaultRecordPageLayoutId } from '@/page-layout/utils/getDefaultRecordPageLayoutId';
import { type TargetRecordIdentifier } from '@/ui/layout/contexts/TargetRecordIdentifier';
import { useFamilySelectorValueV2 } from '@/ui/utilities/state/jotai/hooks/useFamilySelectorValueV2';
import { isDefined } from 'twenty-shared/utils';
export const usePageLayoutIdForRecord = ({
@@ -29,10 +28,9 @@ export const usePageLayoutIdForRecord = ({
objectNameSingular: targetObjectNameSingular,
});
const recordPageLayout = useRecoilValue(
recordPageLayoutByObjectMetadataIdFamilySelector({
objectMetadataId: objectMetadataItem.id,
}),
const recordPageLayout = useFamilySelectorValueV2(
recordPageLayoutByObjectMetadataIdFamilySelector,
{ objectMetadataId: objectMetadataItem.id },
);
if (isDashboard) {
@@ -5,7 +5,7 @@ import { buildWidgetVisibilityContext } from '@/page-layout/utils/buildWidgetVis
import { filterVisibleWidgets } from '@/page-layout/utils/filterVisibleWidgets';
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentValueV2';
import { isDefined } from 'twenty-shared/utils';
export const usePageLayoutTabWithVisibleWidgetsOrThrow = (
@@ -14,7 +14,7 @@ export const usePageLayoutTabWithVisibleWidgetsOrThrow = (
const { currentPageLayout } = useCurrentPageLayout();
const isMobile = useIsMobile();
const { isInRightDrawer } = useLayoutRenderingContext();
const isPageLayoutInEditMode = useRecoilComponentValue(
const isPageLayoutInEditMode = useRecoilComponentValueV2(
isPageLayoutInEditModeComponentState,
);
@@ -2,7 +2,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { recordPageLayoutByObjectMetadataIdFamilySelector } from '@/page-layout/states/selectors/recordPageLayoutByObjectMetadataIdFamilySelector';
import { getDefaultRecordPageLayoutId } from '@/page-layout/utils/getDefaultRecordPageLayoutId';
import { type TargetRecordIdentifier } from '@/ui/layout/contexts/TargetRecordIdentifier';
import { useRecoilValue } from 'recoil';
import { useFamilySelectorValueV2 } from '@/ui/utilities/state/jotai/hooks/useFamilySelectorValueV2';
import { isDefined } from 'twenty-shared/utils';
export const useRecordPageLayoutIdFromRecordStoreOrThrow = ({
@@ -12,10 +12,9 @@ export const useRecordPageLayoutIdFromRecordStoreOrThrow = ({
objectNameSingular: targetObjectNameSingular,
});
const recordPageLayout = useRecoilValue(
recordPageLayoutByObjectMetadataIdFamilySelector({
objectMetadataId: objectMetadataItem.id,
}),
const recordPageLayout = useFamilySelectorValueV2(
recordPageLayoutByObjectMetadataIdFamilySelector,
{ objectMetadataId: objectMetadataItem.id },
);
const pageLayoutId = isDefined(recordPageLayout)
@@ -6,8 +6,9 @@ import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pa
import { removeWidgetFromTab } from '@/page-layout/utils/removeWidgetFromTab';
import { removeWidgetLayoutFromTab } from '@/page-layout/utils/removeWidgetLayoutFromTab';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilCallback } from 'recoil';
import { useRecoilComponentStateCallbackStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentStateCallbackStateV2';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const useRemovePageLayoutWidgetAndPreservePosition = (
@@ -18,80 +19,78 @@ export const useRemovePageLayoutWidgetAndPreservePosition = (
pageLayoutIdFromProps,
);
const pageLayoutDraftState = useRecoilComponentCallbackState(
const pageLayoutDraftState = useRecoilComponentStateCallbackStateV2(
pageLayoutDraftComponentState,
pageLayoutId,
);
const pageLayoutCurrentLayoutsState = useRecoilComponentCallbackState(
const pageLayoutCurrentLayoutsState = useRecoilComponentStateCallbackStateV2(
pageLayoutCurrentLayoutsComponentState,
pageLayoutId,
);
const pageLayoutDraggedAreaState = useRecoilComponentCallbackState(
const pageLayoutDraggedAreaState = useRecoilComponentStateCallbackStateV2(
pageLayoutDraggedAreaComponentState,
pageLayoutId,
);
const pageLayoutEditingWidgetIdState = useRecoilComponentCallbackState(
const pageLayoutEditingWidgetIdState = useRecoilComponentStateCallbackStateV2(
pageLayoutEditingWidgetIdComponentState,
pageLayoutId,
);
const removePageLayoutWidgetAndPreservePosition = useRecoilCallback(
({ snapshot, set }) =>
(widgetId: string) => {
const pageLayoutDraft = snapshot
.getLoadable(pageLayoutDraftState)
.getValue();
const allTabLayouts = snapshot
.getLoadable(pageLayoutCurrentLayoutsState)
.getValue();
const store = useStore();
const tabWithWidget = pageLayoutDraft.tabs.find((tab) =>
tab.widgets.some((w) => w.id === widgetId),
);
const tabId = tabWithWidget?.id;
const removePageLayoutWidgetAndPreservePosition = useCallback(
(widgetId: string) => {
const pageLayoutDraft = store.get(pageLayoutDraftState);
const allTabLayouts = store.get(pageLayoutCurrentLayoutsState);
if (!isDefined(tabId)) {
return;
}
const tabWithWidget = pageLayoutDraft.tabs.find((tab) =>
tab.widgets.some((w) => w.id === widgetId),
);
const tabId = tabWithWidget?.id;
const tabLayouts = allTabLayouts[tabId];
const widgetLayout = tabLayouts?.desktop?.find(
(layout) => layout.i === widgetId,
);
if (!isDefined(tabId)) {
return;
}
if (!isDefined(widgetLayout)) {
return;
}
const tabLayouts = allTabLayouts[tabId];
const widgetLayout = tabLayouts?.desktop?.find(
(layout) => layout.i === widgetId,
);
set(pageLayoutDraggedAreaState, {
x: widgetLayout.x,
y: widgetLayout.y,
w: widgetLayout.w,
h: widgetLayout.h,
});
if (!isDefined(widgetLayout)) {
return;
}
const updatedLayouts = removeWidgetLayoutFromTab(
allTabLayouts,
tabId,
widgetId,
);
set(pageLayoutCurrentLayoutsState, updatedLayouts);
store.set(pageLayoutDraggedAreaState, {
x: widgetLayout.x,
y: widgetLayout.y,
w: widgetLayout.w,
h: widgetLayout.h,
});
set(pageLayoutDraftState, (prev) => ({
...prev,
tabs: removeWidgetFromTab(prev.tabs, tabId, widgetId),
}));
const updatedLayouts = removeWidgetLayoutFromTab(
allTabLayouts,
tabId,
widgetId,
);
store.set(pageLayoutCurrentLayoutsState, updatedLayouts);
set(pageLayoutEditingWidgetIdState, null);
},
store.set(pageLayoutDraftState, (prev) => ({
...prev,
tabs: removeWidgetFromTab(prev.tabs, tabId, widgetId),
}));
store.set(pageLayoutEditingWidgetIdState, null);
},
[
pageLayoutCurrentLayoutsState,
pageLayoutDraftState,
pageLayoutDraggedAreaState,
pageLayoutEditingWidgetIdState,
store,
],
);
@@ -1,9 +1,10 @@
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilComponentStateCallbackStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentStateCallbackStateV2';
import { type DropResult } from '@hello-pangea/dnd';
import { useRecoilCallback } from 'recoil';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const useReorderPageLayoutWidgets = (
@@ -15,33 +16,34 @@ export const useReorderPageLayoutWidgets = (
pageLayoutIdFromProps,
);
const pageLayoutDraftState = useRecoilComponentCallbackState(
const pageLayoutDraftState = useRecoilComponentStateCallbackStateV2(
pageLayoutDraftComponentState,
pageLayoutId,
);
const reorderWidgets = useRecoilCallback(
({ set }) =>
(result: DropResult) => {
if (!result.destination) return;
const store = useStore();
set(pageLayoutDraftState, (prev) => {
const tab = prev.tabs.find((t) => t.id === tabId);
if (!isDefined(tab)) return prev;
const reorderWidgets = useCallback(
(result: DropResult) => {
if (!result.destination) return;
const newWidgets = Array.from(tab.widgets ?? []);
const [removed] = newWidgets.splice(result.source.index, 1);
newWidgets.splice(result.destination!.index, 0, removed);
store.set(pageLayoutDraftState, (prev) => {
const tab = prev.tabs.find((t) => t.id === tabId);
if (!isDefined(tab)) return prev;
return {
...prev,
tabs: prev.tabs.map((t) =>
t.id === tabId ? { ...t, widgets: newWidgets } : t,
),
};
});
},
[tabId, pageLayoutDraftState],
const newWidgets = Array.from(tab.widgets ?? []);
const [removed] = newWidgets.splice(result.source.index, 1);
newWidgets.splice(result.destination!.index, 0, removed);
return {
...prev,
tabs: prev.tabs.map((t) =>
t.id === tabId ? { ...t, widgets: newWidgets } : t,
),
};
});
},
[tabId, pageLayoutDraftState, store],
);
return { reorderWidgets };
@@ -8,9 +8,9 @@ import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLa
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilComponentStateCallbackStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentStateCallbackStateV2';
import { useStore } from 'jotai';
import { useRecoilCallback } from 'recoil';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const useResetDraftPageLayoutToPersistedPageLayout = (
@@ -25,17 +25,17 @@ export const useResetDraftPageLayoutToPersistedPageLayout = (
const tabListComponentInstanceId =
getTabListInstanceIdFromPageLayoutId(componentInstanceId);
const pageLayoutDraftState = useRecoilComponentCallbackState(
const pageLayoutDraftState = useRecoilComponentStateCallbackStateV2(
pageLayoutDraftComponentState,
componentInstanceId,
);
const pageLayoutPersistedState = useRecoilComponentCallbackState(
const pageLayoutPersistedState = useRecoilComponentStateCallbackStateV2(
pageLayoutPersistedComponentState,
componentInstanceId,
);
const pageLayoutCurrentLayoutsState = useRecoilComponentCallbackState(
const pageLayoutCurrentLayoutsState = useRecoilComponentStateCallbackStateV2(
pageLayoutCurrentLayoutsComponentState,
componentInstanceId,
);
@@ -44,64 +44,56 @@ export const useResetDraftPageLayoutToPersistedPageLayout = (
instanceId: tabListComponentInstanceId,
});
const fieldsWidgetGroupsDraftState = useRecoilComponentCallbackState(
const fieldsWidgetGroupsDraftState = useRecoilComponentStateCallbackStateV2(
fieldsWidgetGroupsDraftComponentState,
componentInstanceId,
);
const fieldsWidgetGroupsPersistedState = useRecoilComponentCallbackState(
fieldsWidgetGroupsPersistedComponentState,
componentInstanceId,
);
const fieldsWidgetGroupsPersistedState =
useRecoilComponentStateCallbackStateV2(
fieldsWidgetGroupsPersistedComponentState,
componentInstanceId,
);
const resetDraftPageLayoutToPersistedPageLayout = useRecoilCallback(
({ set, snapshot }) =>
() => {
const pageLayoutPersisted = snapshot
.getLoadable(pageLayoutPersistedState)
.getValue();
const resetDraftPageLayoutToPersistedPageLayout = useCallback(() => {
const pageLayoutPersisted = store.get(pageLayoutPersistedState);
if (isDefined(pageLayoutPersisted)) {
const currentActiveTabId = store.get(activeTabIdAtom);
if (isDefined(pageLayoutPersisted)) {
const currentActiveTabId = store.get(activeTabIdAtom);
const persistedTabIds = pageLayoutPersisted.tabs.map((tab) => tab.id);
const isActiveTabInPersistedTabs =
currentActiveTabId && persistedTabIds.includes(currentActiveTabId);
const persistedTabIds = pageLayoutPersisted.tabs.map((tab) => tab.id);
const isActiveTabInPersistedTabs =
currentActiveTabId && persistedTabIds.includes(currentActiveTabId);
if (
!isActiveTabInPersistedTabs &&
pageLayoutPersisted.tabs.length > 0
) {
store.set(activeTabIdAtom, pageLayoutPersisted.tabs[0].id);
}
if (!isActiveTabInPersistedTabs && pageLayoutPersisted.tabs.length > 0) {
store.set(activeTabIdAtom, pageLayoutPersisted.tabs[0].id);
}
set(pageLayoutDraftState, {
id: pageLayoutPersisted.id,
name: pageLayoutPersisted.name,
type: pageLayoutPersisted.type,
objectMetadataId: pageLayoutPersisted.objectMetadataId,
tabs: pageLayoutPersisted.tabs,
});
store.set(pageLayoutDraftState, {
id: pageLayoutPersisted.id,
name: pageLayoutPersisted.name,
type: pageLayoutPersisted.type,
objectMetadataId: pageLayoutPersisted.objectMetadataId,
tabs: pageLayoutPersisted.tabs,
});
const tabLayouts = convertPageLayoutToTabLayouts(pageLayoutPersisted);
set(pageLayoutCurrentLayoutsState, tabLayouts);
const tabLayouts = convertPageLayoutToTabLayouts(pageLayoutPersisted);
store.set(pageLayoutCurrentLayoutsState, tabLayouts);
const fieldsWidgetGroupsPersisted = snapshot
.getLoadable(fieldsWidgetGroupsPersistedState)
.getValue();
set(fieldsWidgetGroupsDraftState, fieldsWidgetGroupsPersisted);
}
},
[
pageLayoutDraftState,
pageLayoutPersistedState,
pageLayoutCurrentLayoutsState,
fieldsWidgetGroupsDraftState,
fieldsWidgetGroupsPersistedState,
activeTabIdAtom,
store,
],
);
const fieldsWidgetGroupsPersisted = store.get(
fieldsWidgetGroupsPersistedState,
);
store.set(fieldsWidgetGroupsDraftState, fieldsWidgetGroupsPersisted);
}
}, [
pageLayoutDraftState,
pageLayoutPersistedState,
pageLayoutCurrentLayoutsState,
fieldsWidgetGroupsDraftState,
fieldsWidgetGroupsPersistedState,
activeTabIdAtom,
store,
]);
return {
resetDraftPageLayoutToPersistedPageLayout,
@@ -1,10 +1,11 @@
import { UPSERT_FIELDS_WIDGET } from '@/page-layout/graphql/mutations/upsertFieldsWidget';
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
import { fieldsWidgetGroupsPersistedComponentState } from '@/page-layout/states/fieldsWidgetGroupsPersistedComponentState';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilComponentStateCallbackStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentStateCallbackStateV2';
import { useRefreshAllCoreViews } from '@/views/hooks/useRefreshAllCoreViews';
import { useMutation } from '@apollo/client';
import { useRecoilCallback } from 'recoil';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { type ViewFragmentFragment } from '~/generated-metadata/graphql';
@@ -39,15 +40,16 @@ type UseSaveFieldsWidgetGroupsParams = {
export const useSaveFieldsWidgetGroups = ({
pageLayoutId,
}: UseSaveFieldsWidgetGroupsParams) => {
const fieldsWidgetGroupsDraftState = useRecoilComponentCallbackState(
const fieldsWidgetGroupsDraftState = useRecoilComponentStateCallbackStateV2(
fieldsWidgetGroupsDraftComponentState,
pageLayoutId,
);
const fieldsWidgetGroupsPersistedState = useRecoilComponentCallbackState(
fieldsWidgetGroupsPersistedComponentState,
pageLayoutId,
);
const fieldsWidgetGroupsPersistedState =
useRecoilComponentStateCallbackStateV2(
fieldsWidgetGroupsPersistedComponentState,
pageLayoutId,
);
const [upsertFieldsWidgetMutation] = useMutation<
UpsertFieldsWidgetResult,
@@ -56,65 +58,60 @@ export const useSaveFieldsWidgetGroups = ({
const { refreshAllCoreViews } = useRefreshAllCoreViews();
const saveFieldsWidgetGroups = useRecoilCallback(
({ set, snapshot }) =>
async () => {
const allDraftGroups = snapshot
.getLoadable(fieldsWidgetGroupsDraftState)
.getValue();
const allPersistedGroups = snapshot
.getLoadable(fieldsWidgetGroupsPersistedState)
.getValue();
const store = useStore();
const widgetIds = new Set([
...Object.keys(allDraftGroups),
...Object.keys(allPersistedGroups),
]);
const saveFieldsWidgetGroups = useCallback(async () => {
const allDraftGroups = store.get(fieldsWidgetGroupsDraftState);
const allPersistedGroups = store.get(fieldsWidgetGroupsPersistedState);
for (const widgetId of widgetIds) {
const draftGroups = allDraftGroups[widgetId] ?? [];
const widgetIds = new Set([
...Object.keys(allDraftGroups),
...Object.keys(allPersistedGroups),
]);
await upsertFieldsWidgetMutation({
variables: {
input: {
widgetId,
groups: draftGroups.map((group) => ({
id: group.id,
name: group.name,
position: group.position,
isVisible: group.isVisible,
fields: group.fields.flatMap((field) => {
if (!isDefined(field.viewFieldId)) {
return [];
}
for (const widgetId of widgetIds) {
const draftGroups = allDraftGroups[widgetId] ?? [];
return [
{
viewFieldId: field.viewFieldId,
isVisible: field.isVisible,
position: field.position,
},
];
}),
})),
},
},
});
}
await upsertFieldsWidgetMutation({
variables: {
input: {
widgetId,
groups: draftGroups.map((group) => ({
id: group.id,
name: group.name,
position: group.position,
isVisible: group.isVisible,
fields: group.fields.flatMap((field) => {
if (!isDefined(field.viewFieldId)) {
return [];
}
set(fieldsWidgetGroupsPersistedState, allDraftGroups);
return [
{
viewFieldId: field.viewFieldId,
isVisible: field.isVisible,
position: field.position,
},
];
}),
})),
},
},
});
}
await refreshAllCoreViews();
store.set(fieldsWidgetGroupsPersistedState, allDraftGroups);
return { status: 'successful' as const };
},
[
fieldsWidgetGroupsDraftState,
fieldsWidgetGroupsPersistedState,
upsertFieldsWidgetMutation,
refreshAllCoreViews,
],
);
await refreshAllCoreViews();
return { status: 'successful' as const };
}, [
fieldsWidgetGroupsDraftState,
fieldsWidgetGroupsPersistedState,
upsertFieldsWidgetMutation,
refreshAllCoreViews,
store,
]);
return { saveFieldsWidgetGroups };
};
@@ -9,8 +9,9 @@ import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLa
import { reInjectDynamicRelationWidgetsFromDraft } from '@/page-layout/utils/reInjectDynamicRelationWidgetsFromDraft';
import { transformPageLayout } from '@/page-layout/utils/transformPageLayout';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilCallback } from 'recoil';
import { useRecoilComponentStateCallbackStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentStateCallbackStateV2';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { PageLayoutType } from '~/generated-metadata/graphql';
@@ -20,17 +21,19 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
pageLayoutIdFromProps,
);
const pageLayoutPersistedCallbackState = useRecoilComponentCallbackState(
pageLayoutPersistedComponentState,
pageLayoutId,
);
const pageLayoutPersistedCallbackState =
useRecoilComponentStateCallbackStateV2(
pageLayoutPersistedComponentState,
pageLayoutId,
);
const pageLayoutCurrentLayoutsCallbackState = useRecoilComponentCallbackState(
pageLayoutCurrentLayoutsComponentState,
pageLayoutId,
);
const pageLayoutCurrentLayoutsCallbackState =
useRecoilComponentStateCallbackStateV2(
pageLayoutCurrentLayoutsComponentState,
pageLayoutId,
);
const pageLayoutDraftCallbackState = useRecoilComponentCallbackState(
const pageLayoutDraftCallbackState = useRecoilComponentStateCallbackStateV2(
pageLayoutDraftComponentState,
pageLayoutId,
);
@@ -38,54 +41,49 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
const { updatePageLayoutWithTabsAndWidgets } =
useUpdatePageLayoutWithTabsAndWidgets();
const savePageLayout = useRecoilCallback(
({ set, snapshot }) =>
async () => {
const pageLayoutDraft = snapshot
.getLoadable(pageLayoutDraftCallbackState)
.getValue();
const updateInput =
convertPageLayoutDraftToUpdateInput(pageLayoutDraft);
const store = useStore();
const result = await updatePageLayoutWithTabsAndWidgets(
pageLayoutId,
updateInput,
);
const savePageLayout = useCallback(async () => {
const pageLayoutDraft = store.get(pageLayoutDraftCallbackState);
const updateInput = convertPageLayoutDraftToUpdateInput(pageLayoutDraft);
if (result.status === 'successful') {
const updatedPageLayout =
result.response.data?.updatePageLayoutWithTabsAndWidgets;
if (isDefined(updatedPageLayout)) {
const serverLayout: PageLayout =
transformPageLayout(updatedPageLayout);
const pageLayoutToPersist =
serverLayout.type === PageLayoutType.RECORD_PAGE
? reInjectDynamicRelationWidgetsFromDraft(
serverLayout,
pageLayoutDraft,
)
: serverLayout;
set(pageLayoutPersistedCallbackState, pageLayoutToPersist);
set(
pageLayoutCurrentLayoutsCallbackState,
convertPageLayoutToTabLayouts(pageLayoutToPersist),
);
}
}
return result;
},
[
pageLayoutCurrentLayoutsCallbackState,
pageLayoutDraftCallbackState,
const result = await updatePageLayoutWithTabsAndWidgets(
pageLayoutId,
pageLayoutPersistedCallbackState,
updatePageLayoutWithTabsAndWidgets,
],
);
updateInput,
);
if (result.status === 'successful') {
const updatedPageLayout =
result.response.data?.updatePageLayoutWithTabsAndWidgets;
if (isDefined(updatedPageLayout)) {
const serverLayout: PageLayout = transformPageLayout(updatedPageLayout);
const pageLayoutToPersist =
serverLayout.type === PageLayoutType.RECORD_PAGE
? reInjectDynamicRelationWidgetsFromDraft(
serverLayout,
pageLayoutDraft,
)
: serverLayout;
store.set(pageLayoutPersistedCallbackState, pageLayoutToPersist);
store.set(
pageLayoutCurrentLayoutsCallbackState,
convertPageLayoutToTabLayouts(pageLayoutToPersist),
);
}
}
return result;
}, [
pageLayoutCurrentLayoutsCallbackState,
pageLayoutDraftCallbackState,
pageLayoutId,
pageLayoutPersistedCallbackState,
updatePageLayoutWithTabsAndWidgets,
store,
]);
return { savePageLayout };
};
@@ -1,13 +1,15 @@
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { isCommandMenuOpenedStateV2 } from '@/command-menu/states/isCommandMenuOpenedStateV2';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { contextStoreIsPageInEditModeComponentState } from '@/context-store/states/contextStoreIsPageInEditModeComponentState';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { currentPageLayoutIdState } from '@/page-layout/states/currentPageLayoutIdState';
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilCallback } from 'recoil';
import { useRecoilComponentStateCallbackStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentStateCallbackStateV2';
import { useStore } from 'jotai';
import { useCallback } from 'react';
export const useSetIsPageLayoutInEditMode = (pageLayoutIdFromProps: string) => {
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
@@ -15,43 +17,48 @@ export const useSetIsPageLayoutInEditMode = (pageLayoutIdFromProps: string) => {
pageLayoutIdFromProps,
);
const isPageLayoutInEditModeState = useRecoilComponentCallbackState(
const isPageLayoutInEditModeState = useRecoilComponentStateCallbackStateV2(
isPageLayoutInEditModeComponentState,
pageLayoutId,
);
const contextStoreIsFullTabWidgetInEditModeState =
useRecoilComponentCallbackState(
useRecoilComponentStateCallbackStateV2(
contextStoreIsPageInEditModeComponentState,
MAIN_CONTEXT_STORE_INSTANCE_ID,
);
const setIsPageLayoutInEditMode = useRecoilCallback(
({ set, snapshot }) =>
(value: boolean) => {
set(isPageLayoutInEditModeState, value);
const store = useStore();
set(contextStoreIsFullTabWidgetInEditModeState, value);
const setIsPageLayoutInEditMode = useCallback(
(value: boolean) => {
store.set(isPageLayoutInEditModeState, value);
set(currentPageLayoutIdState, value ? pageLayoutId : null);
store.set(contextStoreIsFullTabWidgetInEditModeState, value);
const isCommandMenuOpened = snapshot
.getLoadable(isCommandMenuOpenedState)
.getValue();
jotaiStore.set(
currentPageLayoutIdState.atom,
value ? pageLayoutId : null,
);
if (isCommandMenuOpened) {
set(
contextStoreIsPageInEditModeComponentState.atomFamily({
instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
}),
value,
);
}
},
const isCommandMenuOpened = jotaiStore.get(
isCommandMenuOpenedStateV2.atom,
);
if (isCommandMenuOpened) {
store.set(
contextStoreIsPageInEditModeComponentState.atomFamily({
instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
}),
value,
);
}
},
[
isPageLayoutInEditModeState,
contextStoreIsFullTabWidgetInEditModeState,
pageLayoutId,
store,
],
);
@@ -1,7 +1,8 @@
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilCallback } from 'recoil';
import { useRecoilComponentStateCallbackStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentStateCallbackStateV2';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { pageLayoutSelectedCellsComponentState } from '@/page-layout/states/pageLayoutSelectedCellsComponentState';
export const useStartPageLayoutDragSelection = (
@@ -12,18 +13,16 @@ export const useStartPageLayoutDragSelection = (
pageLayoutIdFromProps,
);
const pageLayoutSelectedCellsState = useRecoilComponentCallbackState(
const pageLayoutSelectedCellsState = useRecoilComponentStateCallbackStateV2(
pageLayoutSelectedCellsComponentState,
pageLayoutId,
);
const startPageLayoutDragSelection = useRecoilCallback(
({ set }) =>
() => {
set(pageLayoutSelectedCellsState, new Set());
},
[pageLayoutSelectedCellsState],
);
const store = useStore();
const startPageLayoutDragSelection = useCallback(() => {
store.set(pageLayoutSelectedCellsState, new Set());
}, [pageLayoutSelectedCellsState, store]);
return { startPageLayoutDragSelection };
};
@@ -1,8 +1,9 @@
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilCallback } from 'recoil';
import { useRecoilComponentStateCallbackStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentStateCallbackStateV2';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
export const useUpdatePageLayoutTab = (pageLayoutIdFromProps?: string) => {
@@ -11,22 +12,23 @@ export const useUpdatePageLayoutTab = (pageLayoutIdFromProps?: string) => {
pageLayoutIdFromProps,
);
const pageLayoutDraftState = useRecoilComponentCallbackState(
const pageLayoutDraftState = useRecoilComponentStateCallbackStateV2(
pageLayoutDraftComponentState,
pageLayoutId,
);
const updatePageLayoutTab = useRecoilCallback(
({ set }) =>
(tabId: string, updates: Partial<PageLayoutTab>) => {
set(pageLayoutDraftState, (prev) => ({
...prev,
tabs: prev.tabs.map((tab) =>
tab.id === tabId ? { ...tab, ...updates } : tab,
),
}));
},
[pageLayoutDraftState],
const store = useStore();
const updatePageLayoutTab = useCallback(
(tabId: string, updates: Partial<PageLayoutTab>) => {
store.set(pageLayoutDraftState, (prev) => ({
...prev,
tabs: prev.tabs.map((tab) =>
tab.id === tabId ? { ...tab, ...updates } : tab,
),
}));
},
[pageLayoutDraftState, store],
);
return { updatePageLayoutTab };
@@ -2,8 +2,9 @@ import { PageLayoutComponentInstanceContext } from '@/page-layout/states/context
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilCallback } from 'recoil';
import { useRecoilComponentStateCallbackStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilComponentStateCallbackStateV2';
import { useStore } from 'jotai';
import { useCallback } from 'react';
export const useUpdatePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
@@ -11,25 +12,26 @@ export const useUpdatePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
pageLayoutIdFromProps,
);
const pageLayoutDraftState = useRecoilComponentCallbackState(
const pageLayoutDraftState = useRecoilComponentStateCallbackStateV2(
pageLayoutDraftComponentState,
pageLayoutId,
);
const updatePageLayoutWidget = useRecoilCallback(
({ set }) =>
(widgetId: string, updates: Partial<PageLayoutWidget>) => {
set(pageLayoutDraftState, (prev) => ({
...prev,
tabs: prev.tabs.map((tab) => ({
...tab,
widgets: tab.widgets.map((widget) =>
widget.id === widgetId ? { ...widget, ...updates } : widget,
),
})),
}));
},
[pageLayoutDraftState],
const store = useStore();
const updatePageLayoutWidget = useCallback(
(widgetId: string, updates: Partial<PageLayoutWidget>) => {
store.set(pageLayoutDraftState, (prev) => ({
...prev,
tabs: prev.tabs.map((tab) => ({
...tab,
widgets: tab.widgets.map((widget) =>
widget.id === widgetId ? { ...widget, ...updates } : widget,
),
})),
}));
},
[pageLayoutDraftState, store],
);
return { updatePageLayoutWidget };