Add Manage and Placement sections in widget side panel page for record page layouts (#19310)
https://github.com/user-attachments/assets/f6120c2e-95e7-4b9b-abb5-69a10c3f2f3b
This commit is contained in:
committed by
GitHub
parent
e048d03872
commit
1f3965e5f8
+164
@@ -0,0 +1,164 @@
|
||||
import { useCanMovePageLayoutWidgetDown } from '@/page-layout/hooks/useCanMovePageLayoutWidgetDown';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { createStore } from 'jotai';
|
||||
import { type ReactNode } from 'react';
|
||||
import {
|
||||
PageLayoutTabLayoutMode,
|
||||
PageLayoutType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import {
|
||||
PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
PageLayoutTestWrapper,
|
||||
} from './PageLayoutTestWrapper';
|
||||
|
||||
const makeWidget = (
|
||||
id: string,
|
||||
index: number,
|
||||
tabId: string = 'tab-1',
|
||||
): PageLayoutWidget =>
|
||||
({
|
||||
id,
|
||||
pageLayoutTabId: tabId,
|
||||
title: id,
|
||||
type: WidgetType.FIELDS,
|
||||
isOverridden: false,
|
||||
gridPosition: { column: 0, columnSpan: 1, row: 0, rowSpan: 1 },
|
||||
configuration: { __typename: 'FieldsConfiguration' as const },
|
||||
position: {
|
||||
__typename: 'PageLayoutWidgetVerticalListPosition' as const,
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
index,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
}) as unknown as PageLayoutWidget;
|
||||
|
||||
const makeTab = (
|
||||
id: string,
|
||||
widgets: PageLayoutWidget[],
|
||||
position: number = 0,
|
||||
layoutMode: PageLayoutTabLayoutMode = PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
) => ({
|
||||
id,
|
||||
applicationId: '',
|
||||
title: id,
|
||||
position,
|
||||
layoutMode,
|
||||
pageLayoutId: '',
|
||||
widgets,
|
||||
isOverridden: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
});
|
||||
|
||||
const makeDraft = (tabs: ReturnType<typeof makeTab>[]): DraftPageLayout => ({
|
||||
id: 'test-layout',
|
||||
name: 'Test Layout',
|
||||
type: PageLayoutType.RECORD_PAGE,
|
||||
objectMetadataId: null,
|
||||
tabs,
|
||||
});
|
||||
|
||||
describe('useCanMovePageLayoutWidgetDown', () => {
|
||||
const getWrapper =
|
||||
(store = createStore()) =>
|
||||
({ children }: { children: ReactNode }) => (
|
||||
<PageLayoutTestWrapper
|
||||
store={store}
|
||||
instanceId={PAGE_LAYOUT_TEST_INSTANCE_ID}
|
||||
>
|
||||
{children}
|
||||
</PageLayoutTestWrapper>
|
||||
);
|
||||
|
||||
const getDraftAtom = () =>
|
||||
pageLayoutDraftComponentState.atomFamily({
|
||||
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
});
|
||||
|
||||
it('should return true when widget is not the last one', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const widgetA = makeWidget('widget-a', 0);
|
||||
const widgetB = makeWidget('widget-b', 1);
|
||||
|
||||
store.set(
|
||||
getDraftAtom(),
|
||||
makeDraft([makeTab('tab-1', [widgetA, widgetB])]),
|
||||
);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useCanMovePageLayoutWidgetDown(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
expect(result.current.canMovePageLayoutWidgetDown('widget-a')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when widget is the last one', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const widgetA = makeWidget('widget-a', 0);
|
||||
const widgetB = makeWidget('widget-b', 1);
|
||||
|
||||
store.set(
|
||||
getDraftAtom(),
|
||||
makeDraft([makeTab('tab-1', [widgetA, widgetB])]),
|
||||
);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useCanMovePageLayoutWidgetDown(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
expect(result.current.canMovePageLayoutWidgetDown('widget-b')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when widget is not found', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const widgetA = makeWidget('widget-a', 0);
|
||||
|
||||
store.set(getDraftAtom(), makeDraft([makeTab('tab-1', [widgetA])]));
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useCanMovePageLayoutWidgetDown(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
expect(result.current.canMovePageLayoutWidgetDown('non-existent')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false for non-VERTICAL_LIST tab', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const widgetA = makeWidget('widget-a', 0);
|
||||
const widgetB = makeWidget('widget-b', 1);
|
||||
|
||||
store.set(
|
||||
getDraftAtom(),
|
||||
makeDraft([
|
||||
makeTab('tab-1', [widgetA, widgetB], 0, PageLayoutTabLayoutMode.CANVAS),
|
||||
]),
|
||||
);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useCanMovePageLayoutWidgetDown(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
expect(result.current.canMovePageLayoutWidgetDown('widget-a')).toBe(false);
|
||||
});
|
||||
});
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
import { useCanMovePageLayoutWidgetUp } from '@/page-layout/hooks/useCanMovePageLayoutWidgetUp';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { createStore } from 'jotai';
|
||||
import { type ReactNode } from 'react';
|
||||
import {
|
||||
PageLayoutTabLayoutMode,
|
||||
PageLayoutType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import {
|
||||
PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
PageLayoutTestWrapper,
|
||||
} from './PageLayoutTestWrapper';
|
||||
|
||||
const makeWidget = (
|
||||
id: string,
|
||||
index: number,
|
||||
tabId: string = 'tab-1',
|
||||
): PageLayoutWidget =>
|
||||
({
|
||||
id,
|
||||
pageLayoutTabId: tabId,
|
||||
title: id,
|
||||
type: WidgetType.FIELDS,
|
||||
isOverridden: false,
|
||||
gridPosition: { column: 0, columnSpan: 1, row: 0, rowSpan: 1 },
|
||||
configuration: { __typename: 'FieldsConfiguration' as const },
|
||||
position: {
|
||||
__typename: 'PageLayoutWidgetVerticalListPosition' as const,
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
index,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
}) as unknown as PageLayoutWidget;
|
||||
|
||||
const makeTab = (
|
||||
id: string,
|
||||
widgets: PageLayoutWidget[],
|
||||
position: number = 0,
|
||||
layoutMode: PageLayoutTabLayoutMode = PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
) => ({
|
||||
id,
|
||||
applicationId: '',
|
||||
title: id,
|
||||
position,
|
||||
layoutMode,
|
||||
pageLayoutId: '',
|
||||
widgets,
|
||||
isOverridden: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
});
|
||||
|
||||
const makeDraft = (tabs: ReturnType<typeof makeTab>[]): DraftPageLayout => ({
|
||||
id: 'test-layout',
|
||||
name: 'Test Layout',
|
||||
type: PageLayoutType.RECORD_PAGE,
|
||||
objectMetadataId: null,
|
||||
tabs,
|
||||
});
|
||||
|
||||
describe('useCanMovePageLayoutWidgetUp', () => {
|
||||
const getWrapper =
|
||||
(store = createStore()) =>
|
||||
({ children }: { children: ReactNode }) => (
|
||||
<PageLayoutTestWrapper
|
||||
store={store}
|
||||
instanceId={PAGE_LAYOUT_TEST_INSTANCE_ID}
|
||||
>
|
||||
{children}
|
||||
</PageLayoutTestWrapper>
|
||||
);
|
||||
|
||||
const getDraftAtom = () =>
|
||||
pageLayoutDraftComponentState.atomFamily({
|
||||
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
});
|
||||
|
||||
it('should return true when widget is not the first one', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const widgetA = makeWidget('widget-a', 0);
|
||||
const widgetB = makeWidget('widget-b', 1);
|
||||
|
||||
store.set(
|
||||
getDraftAtom(),
|
||||
makeDraft([makeTab('tab-1', [widgetA, widgetB])]),
|
||||
);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useCanMovePageLayoutWidgetUp(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
expect(result.current.canMovePageLayoutWidgetUp('widget-b')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when widget is the first one', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const widgetA = makeWidget('widget-a', 0);
|
||||
const widgetB = makeWidget('widget-b', 1);
|
||||
|
||||
store.set(
|
||||
getDraftAtom(),
|
||||
makeDraft([makeTab('tab-1', [widgetA, widgetB])]),
|
||||
);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useCanMovePageLayoutWidgetUp(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
expect(result.current.canMovePageLayoutWidgetUp('widget-a')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when widget is not found', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const widgetA = makeWidget('widget-a', 0);
|
||||
|
||||
store.set(getDraftAtom(), makeDraft([makeTab('tab-1', [widgetA])]));
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useCanMovePageLayoutWidgetUp(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
expect(result.current.canMovePageLayoutWidgetUp('non-existent')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false for non-VERTICAL_LIST tab', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const widgetA = makeWidget('widget-a', 0);
|
||||
const widgetB = makeWidget('widget-b', 1);
|
||||
|
||||
store.set(
|
||||
getDraftAtom(),
|
||||
makeDraft([
|
||||
makeTab('tab-1', [widgetA, widgetB], 0, PageLayoutTabLayoutMode.CANVAS),
|
||||
]),
|
||||
);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useCanMovePageLayoutWidgetUp(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
expect(result.current.canMovePageLayoutWidgetUp('widget-b')).toBe(false);
|
||||
});
|
||||
});
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
|
||||
import { useCreateWidgetFromClick } from '@/page-layout/hooks/useCreateWidgetFromClick';
|
||||
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { type ReactNode } from 'react';
|
||||
@@ -28,7 +28,7 @@ describe('useCreateWidgetFromClick', () => {
|
||||
it('should set dragged area and navigate to widget selection when called with a cellId', () => {
|
||||
const { result } = renderHook(
|
||||
() => ({
|
||||
createWidget: useCreateWidgetFromClick(),
|
||||
createWidget: useCreateWidgetFromClick(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
draggedArea: useAtomComponentStateValue(
|
||||
pageLayoutDraggedAreaComponentState,
|
||||
PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
@@ -52,7 +52,7 @@ describe('useCreateWidgetFromClick', () => {
|
||||
expect(result.current.draggedArea).toEqual({ x: 2, y: 3, w: 1, h: 1 });
|
||||
expect(result.current.editingWidgetId).toBeNull();
|
||||
expect(mockNavigatePageLayoutSidePanel).toHaveBeenCalledWith({
|
||||
sidePanelPage: SidePanelPages.PageLayoutWidgetTypeSelect,
|
||||
sidePanelPage: SidePanelPages.PageLayoutDashboardWidgetTypeSelect,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
});
|
||||
|
||||
+13
-4
@@ -1,12 +1,12 @@
|
||||
import { useDeletePageLayoutWidget } from '@/page-layout/hooks/useDeletePageLayoutWidget';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { createStore } from 'jotai';
|
||||
import { type ReactNode } from 'react';
|
||||
import {
|
||||
PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
PageLayoutTestWrapper,
|
||||
} from './PageLayoutTestWrapper';
|
||||
import { useDeletePageLayoutWidget } from '@/page-layout/hooks/useDeletePageLayoutWidget';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { createStore } from 'jotai';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
jest.mock(
|
||||
'@/page-layout/widgets/record-table/hooks/useDeleteViewForRecordTableWidget',
|
||||
@@ -17,6 +17,15 @@ jest.mock(
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'@/page-layout/widgets/fields/hooks/useDeleteViewForFieldsWidget',
|
||||
() => ({
|
||||
useDeleteViewForFieldsWidget: () => ({
|
||||
deleteViewForFieldsWidget: jest.fn(),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock('@/side-panel/hooks/useSidePanelMenu', () => ({
|
||||
useSidePanelMenu: () => ({
|
||||
closeSidePanelMenu: jest.fn(),
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
|
||||
import { useEndPageLayoutDragSelection } from '@/page-layout/hooks/useEndPageLayoutDragSelection';
|
||||
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
|
||||
import { pageLayoutSelectedCellsComponentState } from '@/page-layout/states/pageLayoutSelectedCellsComponentState';
|
||||
import { calculateGridBoundsFromSelectedCells } from '@/page-layout/utils/calculateGridBoundsFromSelectedCells';
|
||||
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { createStore } from 'jotai';
|
||||
@@ -102,7 +102,7 @@ describe('useEndPageLayoutDragSelection', () => {
|
||||
expect(result.current.selectedCells.size).toBe(0);
|
||||
|
||||
expect(mockNavigatePageLayoutSidePanel).toHaveBeenCalledWith({
|
||||
sidePanelPage: SidePanelPages.PageLayoutWidgetTypeSelect,
|
||||
sidePanelPage: SidePanelPages.PageLayoutDashboardWidgetTypeSelect,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
});
|
||||
|
||||
+342
@@ -0,0 +1,342 @@
|
||||
import { useInsertCreatedWidgetAtContext } from '@/page-layout/hooks/useInsertCreatedWidgetAtContext';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { widgetInsertionContextComponentState } from '@/page-layout/states/widgetInsertionContextComponentState';
|
||||
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { createStore } from 'jotai';
|
||||
import { type ReactNode } from 'react';
|
||||
import {
|
||||
PageLayoutTabLayoutMode,
|
||||
PageLayoutType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import {
|
||||
PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
PageLayoutTestWrapper,
|
||||
} from './PageLayoutTestWrapper';
|
||||
|
||||
const makeWidget = (
|
||||
id: string,
|
||||
index: number,
|
||||
tabId: string = 'tab-1',
|
||||
): PageLayoutWidget =>
|
||||
({
|
||||
id,
|
||||
pageLayoutTabId: tabId,
|
||||
title: id,
|
||||
type: WidgetType.FIELDS,
|
||||
isOverridden: false,
|
||||
gridPosition: { column: 0, columnSpan: 1, row: 0, rowSpan: 1 },
|
||||
configuration: { __typename: 'FieldsConfiguration' as const },
|
||||
position: {
|
||||
__typename: 'PageLayoutWidgetVerticalListPosition' as const,
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
index,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
}) as unknown as PageLayoutWidget;
|
||||
|
||||
const makeTab = (
|
||||
id: string,
|
||||
widgets: PageLayoutWidget[],
|
||||
position: number = 0,
|
||||
) => ({
|
||||
id,
|
||||
applicationId: '',
|
||||
title: id,
|
||||
position,
|
||||
pageLayoutId: '',
|
||||
widgets,
|
||||
isOverridden: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
});
|
||||
|
||||
const makeDraft = (tabs: ReturnType<typeof makeTab>[]): DraftPageLayout => ({
|
||||
id: 'test-layout',
|
||||
name: 'Test Layout',
|
||||
type: PageLayoutType.RECORD_PAGE,
|
||||
objectMetadataId: null,
|
||||
tabs,
|
||||
});
|
||||
|
||||
describe('useInsertCreatedWidgetAtContext', () => {
|
||||
const getWrapper =
|
||||
(store = createStore()) =>
|
||||
({ children }: { children: ReactNode }) => (
|
||||
<PageLayoutTestWrapper
|
||||
store={store}
|
||||
instanceId={PAGE_LAYOUT_TEST_INSTANCE_ID}
|
||||
>
|
||||
{children}
|
||||
</PageLayoutTestWrapper>
|
||||
);
|
||||
|
||||
const getDraftAtom = () =>
|
||||
pageLayoutDraftComponentState.atomFamily({
|
||||
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
});
|
||||
|
||||
const getInsertionContextAtom = () =>
|
||||
widgetInsertionContextComponentState.atomFamily({
|
||||
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
});
|
||||
|
||||
it('should insert widget above target', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const widgetA = makeWidget('widget-a', 0);
|
||||
const widgetB = makeWidget('widget-b', 1);
|
||||
const widgetC = makeWidget('widget-c', 2);
|
||||
|
||||
store.set(
|
||||
getDraftAtom(),
|
||||
makeDraft([makeTab('tab-1', [widgetA, widgetB, widgetC])]),
|
||||
);
|
||||
store.set(getInsertionContextAtom(), {
|
||||
targetWidgetId: 'widget-b',
|
||||
direction: 'above',
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useInsertCreatedWidgetAtContext(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.insertCreatedWidgetAtContext('widget-c');
|
||||
});
|
||||
|
||||
const draft = store.get(getDraftAtom());
|
||||
const widgetIds = draft.tabs[0].widgets.map((w) => w.id);
|
||||
|
||||
expect(widgetIds).toEqual(['widget-a', 'widget-c', 'widget-b']);
|
||||
});
|
||||
|
||||
it('should insert widget below target', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const widgetA = makeWidget('widget-a', 0);
|
||||
const widgetB = makeWidget('widget-b', 1);
|
||||
const widgetC = makeWidget('widget-c', 2);
|
||||
|
||||
store.set(
|
||||
getDraftAtom(),
|
||||
makeDraft([makeTab('tab-1', [widgetA, widgetB, widgetC])]),
|
||||
);
|
||||
store.set(getInsertionContextAtom(), {
|
||||
targetWidgetId: 'widget-a',
|
||||
direction: 'below',
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useInsertCreatedWidgetAtContext(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.insertCreatedWidgetAtContext('widget-c');
|
||||
});
|
||||
|
||||
const draft = store.get(getDraftAtom());
|
||||
const widgetIds = draft.tabs[0].widgets.map((w) => w.id);
|
||||
|
||||
expect(widgetIds).toEqual(['widget-a', 'widget-c', 'widget-b']);
|
||||
});
|
||||
|
||||
it('should reindex all widgets with sequential positions', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const widgetA = makeWidget('widget-a', 0);
|
||||
const widgetB = makeWidget('widget-b', 1);
|
||||
const widgetC = makeWidget('widget-c', 2);
|
||||
|
||||
store.set(
|
||||
getDraftAtom(),
|
||||
makeDraft([makeTab('tab-1', [widgetA, widgetB, widgetC])]),
|
||||
);
|
||||
store.set(getInsertionContextAtom(), {
|
||||
targetWidgetId: 'widget-a',
|
||||
direction: 'above',
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useInsertCreatedWidgetAtContext(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.insertCreatedWidgetAtContext('widget-c');
|
||||
});
|
||||
|
||||
const draft = store.get(getDraftAtom());
|
||||
const positions = draft.tabs[0].widgets.map((w) => w.position);
|
||||
|
||||
positions.forEach((position, index) => {
|
||||
expect(position).toEqual({
|
||||
__typename: 'PageLayoutWidgetVerticalListPosition',
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
index,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should no-op when insertion context is null', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const widgetA = makeWidget('widget-a', 0);
|
||||
const widgetB = makeWidget('widget-b', 1);
|
||||
|
||||
const initialDraft = makeDraft([makeTab('tab-1', [widgetA, widgetB])]);
|
||||
store.set(getDraftAtom(), initialDraft);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useInsertCreatedWidgetAtContext(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.insertCreatedWidgetAtContext('widget-b');
|
||||
});
|
||||
|
||||
const draft = store.get(getDraftAtom());
|
||||
|
||||
expect(draft).toBe(initialDraft);
|
||||
});
|
||||
|
||||
it('should no-op when target widget is not found', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const widgetA = makeWidget('widget-a', 0);
|
||||
const widgetB = makeWidget('widget-b', 1);
|
||||
|
||||
const initialDraft = makeDraft([makeTab('tab-1', [widgetA, widgetB])]);
|
||||
store.set(getDraftAtom(), initialDraft);
|
||||
store.set(getInsertionContextAtom(), {
|
||||
targetWidgetId: 'non-existent',
|
||||
direction: 'above',
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useInsertCreatedWidgetAtContext(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.insertCreatedWidgetAtContext('widget-b');
|
||||
});
|
||||
|
||||
const draft = store.get(getDraftAtom());
|
||||
|
||||
expect(draft).toBe(initialDraft);
|
||||
});
|
||||
|
||||
it('should no-op when new widget does not exist in tab', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const widgetA = makeWidget('widget-a', 0);
|
||||
const widgetB = makeWidget('widget-b', 1);
|
||||
|
||||
const initialDraft = makeDraft([makeTab('tab-1', [widgetA, widgetB])]);
|
||||
store.set(getDraftAtom(), initialDraft);
|
||||
store.set(getInsertionContextAtom(), {
|
||||
targetWidgetId: 'widget-a',
|
||||
direction: 'below',
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useInsertCreatedWidgetAtContext(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.insertCreatedWidgetAtContext('non-existent-widget');
|
||||
});
|
||||
|
||||
const draft = store.get(getDraftAtom());
|
||||
const widgetIds = draft.tabs[0].widgets.map((w) => w.id);
|
||||
|
||||
expect(widgetIds).toEqual(['widget-a', 'widget-b']);
|
||||
});
|
||||
|
||||
it('should clear insertion context after insertion', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const widgetA = makeWidget('widget-a', 0);
|
||||
const widgetB = makeWidget('widget-b', 1);
|
||||
|
||||
store.set(
|
||||
getDraftAtom(),
|
||||
makeDraft([makeTab('tab-1', [widgetA, widgetB])]),
|
||||
);
|
||||
store.set(getInsertionContextAtom(), {
|
||||
targetWidgetId: 'widget-a',
|
||||
direction: 'below',
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useInsertCreatedWidgetAtContext(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.insertCreatedWidgetAtContext('widget-b');
|
||||
});
|
||||
|
||||
const insertionContext = store.get(getInsertionContextAtom());
|
||||
|
||||
expect(insertionContext).toBeNull();
|
||||
});
|
||||
|
||||
it('should only modify the tab containing the target widget', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const tab1WidgetA = makeWidget('widget-a', 0, 'tab-1');
|
||||
const tab1WidgetB = makeWidget('widget-b', 1, 'tab-1');
|
||||
const tab1WidgetC = makeWidget('widget-c', 2, 'tab-1');
|
||||
|
||||
const tab2WidgetX = makeWidget('widget-x', 0, 'tab-2');
|
||||
const tab2WidgetY = makeWidget('widget-y', 1, 'tab-2');
|
||||
|
||||
store.set(
|
||||
getDraftAtom(),
|
||||
makeDraft([
|
||||
makeTab('tab-1', [tab1WidgetA, tab1WidgetB, tab1WidgetC], 0),
|
||||
makeTab('tab-2', [tab2WidgetX, tab2WidgetY], 1),
|
||||
]),
|
||||
);
|
||||
store.set(getInsertionContextAtom(), {
|
||||
targetWidgetId: 'widget-a',
|
||||
direction: 'above',
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useInsertCreatedWidgetAtContext(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.insertCreatedWidgetAtContext('widget-c');
|
||||
});
|
||||
|
||||
const draft = store.get(getDraftAtom());
|
||||
|
||||
const tab1WidgetIds = draft.tabs[0].widgets.map((w) => w.id);
|
||||
expect(tab1WidgetIds).toEqual(['widget-c', 'widget-a', 'widget-b']);
|
||||
|
||||
const tab2WidgetIds = draft.tabs[1].widgets.map((w) => w.id);
|
||||
expect(tab2WidgetIds).toEqual(['widget-x', 'widget-y']);
|
||||
});
|
||||
});
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
import { useMovePageLayoutWidgetDown } from '@/page-layout/hooks/useMovePageLayoutWidgetDown';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { createStore } from 'jotai';
|
||||
import { type ReactNode } from 'react';
|
||||
import {
|
||||
PageLayoutTabLayoutMode,
|
||||
PageLayoutType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import {
|
||||
PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
PageLayoutTestWrapper,
|
||||
} from './PageLayoutTestWrapper';
|
||||
|
||||
const makeWidget = (
|
||||
id: string,
|
||||
index: number,
|
||||
tabId: string = 'tab-1',
|
||||
): PageLayoutWidget =>
|
||||
({
|
||||
id,
|
||||
pageLayoutTabId: tabId,
|
||||
title: id,
|
||||
type: WidgetType.FIELDS,
|
||||
isOverridden: false,
|
||||
gridPosition: { column: 0, columnSpan: 1, row: 0, rowSpan: 1 },
|
||||
configuration: { __typename: 'FieldsConfiguration' as const },
|
||||
position: {
|
||||
__typename: 'PageLayoutWidgetVerticalListPosition' as const,
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
index,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
}) as unknown as PageLayoutWidget;
|
||||
|
||||
const makeTab = (
|
||||
id: string,
|
||||
widgets: PageLayoutWidget[],
|
||||
position: number = 0,
|
||||
) => ({
|
||||
id,
|
||||
applicationId: '',
|
||||
title: id,
|
||||
position,
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
pageLayoutId: '',
|
||||
widgets,
|
||||
isOverridden: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
});
|
||||
|
||||
const makeDraft = (tabs: ReturnType<typeof makeTab>[]): DraftPageLayout => ({
|
||||
id: 'test-layout',
|
||||
name: 'Test Layout',
|
||||
type: PageLayoutType.RECORD_PAGE,
|
||||
objectMetadataId: null,
|
||||
tabs,
|
||||
});
|
||||
|
||||
describe('useMovePageLayoutWidgetDown', () => {
|
||||
const getWrapper =
|
||||
(store = createStore()) =>
|
||||
({ children }: { children: ReactNode }) => (
|
||||
<PageLayoutTestWrapper
|
||||
store={store}
|
||||
instanceId={PAGE_LAYOUT_TEST_INSTANCE_ID}
|
||||
>
|
||||
{children}
|
||||
</PageLayoutTestWrapper>
|
||||
);
|
||||
|
||||
const getDraftAtom = () =>
|
||||
pageLayoutDraftComponentState.atomFamily({
|
||||
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
});
|
||||
|
||||
it('should swap widget with the one below it', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const widgetA = makeWidget('widget-a', 0);
|
||||
const widgetB = makeWidget('widget-b', 1);
|
||||
const widgetC = makeWidget('widget-c', 2);
|
||||
|
||||
store.set(
|
||||
getDraftAtom(),
|
||||
makeDraft([makeTab('tab-1', [widgetA, widgetB, widgetC])]),
|
||||
);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useMovePageLayoutWidgetDown(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.movePageLayoutWidgetDown('widget-a');
|
||||
});
|
||||
|
||||
const draft = store.get(getDraftAtom());
|
||||
const widgets = draft.tabs[0].widgets;
|
||||
|
||||
const widgetAPosition = widgets.find((w) => w.id === 'widget-a')?.position;
|
||||
const widgetBPosition = widgets.find((w) => w.id === 'widget-b')?.position;
|
||||
|
||||
expect(widgetAPosition).toEqual(expect.objectContaining({ index: 1 }));
|
||||
expect(widgetBPosition).toEqual(expect.objectContaining({ index: 0 }));
|
||||
});
|
||||
|
||||
it('should not change draft when widget is at the bottom', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const widgetA = makeWidget('widget-a', 0);
|
||||
const widgetB = makeWidget('widget-b', 1);
|
||||
|
||||
const initialDraft = makeDraft([makeTab('tab-1', [widgetA, widgetB])]);
|
||||
store.set(getDraftAtom(), initialDraft);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useMovePageLayoutWidgetDown(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.movePageLayoutWidgetDown('widget-b');
|
||||
});
|
||||
|
||||
const draft = store.get(getDraftAtom());
|
||||
|
||||
expect(draft).toBe(initialDraft);
|
||||
});
|
||||
|
||||
it('should not change draft when widget is not found', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const widgetA = makeWidget('widget-a', 0);
|
||||
const initialDraft = makeDraft([makeTab('tab-1', [widgetA])]);
|
||||
store.set(getDraftAtom(), initialDraft);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useMovePageLayoutWidgetDown(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.movePageLayoutWidgetDown('non-existent');
|
||||
});
|
||||
|
||||
const draft = store.get(getDraftAtom());
|
||||
|
||||
expect(draft).toBe(initialDraft);
|
||||
});
|
||||
|
||||
it('should only modify the correct tab', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const tab1WidgetA = makeWidget('widget-a', 0, 'tab-1');
|
||||
const tab1WidgetB = makeWidget('widget-b', 1, 'tab-1');
|
||||
const tab2WidgetX = makeWidget('widget-x', 0, 'tab-2');
|
||||
|
||||
store.set(
|
||||
getDraftAtom(),
|
||||
makeDraft([
|
||||
makeTab('tab-1', [tab1WidgetA, tab1WidgetB], 0),
|
||||
makeTab('tab-2', [tab2WidgetX], 1),
|
||||
]),
|
||||
);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useMovePageLayoutWidgetDown(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.movePageLayoutWidgetDown('widget-a');
|
||||
});
|
||||
|
||||
const draft = store.get(getDraftAtom());
|
||||
|
||||
expect(draft.tabs[1].widgets[0].id).toBe('widget-x');
|
||||
});
|
||||
});
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
import { useMovePageLayoutWidgetUp } from '@/page-layout/hooks/useMovePageLayoutWidgetUp';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { createStore } from 'jotai';
|
||||
import { type ReactNode } from 'react';
|
||||
import {
|
||||
PageLayoutTabLayoutMode,
|
||||
PageLayoutType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import {
|
||||
PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
PageLayoutTestWrapper,
|
||||
} from './PageLayoutTestWrapper';
|
||||
|
||||
const makeWidget = (
|
||||
id: string,
|
||||
index: number,
|
||||
tabId: string = 'tab-1',
|
||||
): PageLayoutWidget =>
|
||||
({
|
||||
id,
|
||||
pageLayoutTabId: tabId,
|
||||
title: id,
|
||||
type: WidgetType.FIELDS,
|
||||
isOverridden: false,
|
||||
gridPosition: { column: 0, columnSpan: 1, row: 0, rowSpan: 1 },
|
||||
configuration: { __typename: 'FieldsConfiguration' as const },
|
||||
position: {
|
||||
__typename: 'PageLayoutWidgetVerticalListPosition' as const,
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
index,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
}) as unknown as PageLayoutWidget;
|
||||
|
||||
const makeTab = (
|
||||
id: string,
|
||||
widgets: PageLayoutWidget[],
|
||||
position: number = 0,
|
||||
) => ({
|
||||
id,
|
||||
applicationId: '',
|
||||
title: id,
|
||||
position,
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
pageLayoutId: '',
|
||||
widgets,
|
||||
isOverridden: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
});
|
||||
|
||||
const makeDraft = (tabs: ReturnType<typeof makeTab>[]): DraftPageLayout => ({
|
||||
id: 'test-layout',
|
||||
name: 'Test Layout',
|
||||
type: PageLayoutType.RECORD_PAGE,
|
||||
objectMetadataId: null,
|
||||
tabs,
|
||||
});
|
||||
|
||||
describe('useMovePageLayoutWidgetUp', () => {
|
||||
const getWrapper =
|
||||
(store = createStore()) =>
|
||||
({ children }: { children: ReactNode }) => (
|
||||
<PageLayoutTestWrapper
|
||||
store={store}
|
||||
instanceId={PAGE_LAYOUT_TEST_INSTANCE_ID}
|
||||
>
|
||||
{children}
|
||||
</PageLayoutTestWrapper>
|
||||
);
|
||||
|
||||
const getDraftAtom = () =>
|
||||
pageLayoutDraftComponentState.atomFamily({
|
||||
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
});
|
||||
|
||||
it('should swap widget with the one above it', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const widgetA = makeWidget('widget-a', 0);
|
||||
const widgetB = makeWidget('widget-b', 1);
|
||||
const widgetC = makeWidget('widget-c', 2);
|
||||
|
||||
store.set(
|
||||
getDraftAtom(),
|
||||
makeDraft([makeTab('tab-1', [widgetA, widgetB, widgetC])]),
|
||||
);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useMovePageLayoutWidgetUp(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.movePageLayoutWidgetUp('widget-b');
|
||||
});
|
||||
|
||||
const draft = store.get(getDraftAtom());
|
||||
const widgets = draft.tabs[0].widgets;
|
||||
|
||||
const widgetAPosition = widgets.find((w) => w.id === 'widget-a')?.position;
|
||||
const widgetBPosition = widgets.find((w) => w.id === 'widget-b')?.position;
|
||||
|
||||
expect(widgetBPosition).toEqual(expect.objectContaining({ index: 0 }));
|
||||
expect(widgetAPosition).toEqual(expect.objectContaining({ index: 1 }));
|
||||
});
|
||||
|
||||
it('should not change draft when widget is already at the top', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const widgetA = makeWidget('widget-a', 0);
|
||||
const widgetB = makeWidget('widget-b', 1);
|
||||
|
||||
const initialDraft = makeDraft([makeTab('tab-1', [widgetA, widgetB])]);
|
||||
store.set(getDraftAtom(), initialDraft);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useMovePageLayoutWidgetUp(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.movePageLayoutWidgetUp('widget-a');
|
||||
});
|
||||
|
||||
const draft = store.get(getDraftAtom());
|
||||
|
||||
expect(draft).toBe(initialDraft);
|
||||
});
|
||||
|
||||
it('should not change draft when widget is not found', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const widgetA = makeWidget('widget-a', 0);
|
||||
const initialDraft = makeDraft([makeTab('tab-1', [widgetA])]);
|
||||
store.set(getDraftAtom(), initialDraft);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useMovePageLayoutWidgetUp(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.movePageLayoutWidgetUp('non-existent');
|
||||
});
|
||||
|
||||
const draft = store.get(getDraftAtom());
|
||||
|
||||
expect(draft).toBe(initialDraft);
|
||||
});
|
||||
|
||||
it('should only modify the correct tab', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const tab1WidgetA = makeWidget('widget-a', 0, 'tab-1');
|
||||
const tab1WidgetB = makeWidget('widget-b', 1, 'tab-1');
|
||||
const tab2WidgetX = makeWidget('widget-x', 0, 'tab-2');
|
||||
|
||||
store.set(
|
||||
getDraftAtom(),
|
||||
makeDraft([
|
||||
makeTab('tab-1', [tab1WidgetA, tab1WidgetB], 0),
|
||||
makeTab('tab-2', [tab2WidgetX], 1),
|
||||
]),
|
||||
);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useMovePageLayoutWidgetUp(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.movePageLayoutWidgetUp('widget-b');
|
||||
});
|
||||
|
||||
const draft = store.get(getDraftAtom());
|
||||
|
||||
expect(draft.tabs[1].widgets[0].id).toBe('widget-x');
|
||||
});
|
||||
});
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
import { useMoveWidgetToTab } from '@/page-layout/hooks/useMoveWidgetToTab';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { createStore } from 'jotai';
|
||||
import { type ReactNode } from 'react';
|
||||
import {
|
||||
PageLayoutTabLayoutMode,
|
||||
PageLayoutType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import {
|
||||
PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
PageLayoutTestWrapper,
|
||||
} from './PageLayoutTestWrapper';
|
||||
|
||||
const makeWidget = (
|
||||
id: string,
|
||||
index: number,
|
||||
tabId: string = 'tab-1',
|
||||
): PageLayoutWidget =>
|
||||
({
|
||||
id,
|
||||
pageLayoutTabId: tabId,
|
||||
title: id,
|
||||
type: WidgetType.FIELDS,
|
||||
isOverridden: false,
|
||||
gridPosition: { column: 0, columnSpan: 1, row: 0, rowSpan: 1 },
|
||||
configuration: { __typename: 'FieldsConfiguration' as const },
|
||||
position: {
|
||||
__typename: 'PageLayoutWidgetVerticalListPosition' as const,
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
index,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
}) as unknown as PageLayoutWidget;
|
||||
|
||||
const makeTab = (
|
||||
id: string,
|
||||
widgets: PageLayoutWidget[],
|
||||
position: number = 0,
|
||||
) => ({
|
||||
id,
|
||||
applicationId: '',
|
||||
title: id,
|
||||
position,
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
pageLayoutId: '',
|
||||
widgets,
|
||||
isOverridden: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
});
|
||||
|
||||
const makeDraft = (tabs: ReturnType<typeof makeTab>[]): DraftPageLayout => ({
|
||||
id: 'test-layout',
|
||||
name: 'Test Layout',
|
||||
type: PageLayoutType.RECORD_PAGE,
|
||||
objectMetadataId: null,
|
||||
tabs,
|
||||
});
|
||||
|
||||
describe('useMoveWidgetToTab', () => {
|
||||
const getWrapper =
|
||||
(store = createStore()) =>
|
||||
({ children }: { children: ReactNode }) => (
|
||||
<PageLayoutTestWrapper
|
||||
store={store}
|
||||
instanceId={PAGE_LAYOUT_TEST_INSTANCE_ID}
|
||||
>
|
||||
{children}
|
||||
</PageLayoutTestWrapper>
|
||||
);
|
||||
|
||||
const getDraftAtom = () =>
|
||||
pageLayoutDraftComponentState.atomFamily({
|
||||
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
});
|
||||
|
||||
it('should move widget from source tab to destination tab', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const widgetA = makeWidget('widget-a', 0, 'tab-1');
|
||||
const widgetB = makeWidget('widget-b', 1, 'tab-1');
|
||||
const widgetX = makeWidget('widget-x', 0, 'tab-2');
|
||||
|
||||
store.set(
|
||||
getDraftAtom(),
|
||||
makeDraft([
|
||||
makeTab('tab-1', [widgetA, widgetB], 0),
|
||||
makeTab('tab-2', [widgetX], 1),
|
||||
]),
|
||||
);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useMoveWidgetToTab(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.moveWidgetToTab('widget-a', 'tab-2');
|
||||
});
|
||||
|
||||
const draft = store.get(getDraftAtom());
|
||||
|
||||
const tab1WidgetIds = draft.tabs[0].widgets.map((w) => w.id);
|
||||
expect(tab1WidgetIds).toEqual(['widget-b']);
|
||||
|
||||
const tab2WidgetIds = draft.tabs[1].widgets.map((w) => w.id);
|
||||
expect(tab2WidgetIds).toEqual(['widget-x', 'widget-a']);
|
||||
});
|
||||
|
||||
it('should reindex remaining widgets in source tab', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const widgetA = makeWidget('widget-a', 0, 'tab-1');
|
||||
const widgetB = makeWidget('widget-b', 1, 'tab-1');
|
||||
const widgetC = makeWidget('widget-c', 2, 'tab-1');
|
||||
|
||||
store.set(
|
||||
getDraftAtom(),
|
||||
makeDraft([
|
||||
makeTab('tab-1', [widgetA, widgetB, widgetC], 0),
|
||||
makeTab('tab-2', [], 1),
|
||||
]),
|
||||
);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useMoveWidgetToTab(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.moveWidgetToTab('widget-a', 'tab-2');
|
||||
});
|
||||
|
||||
const draft = store.get(getDraftAtom());
|
||||
|
||||
const tab1Positions = draft.tabs[0].widgets.map((w) => {
|
||||
if (w.position && 'index' in w.position) {
|
||||
return w.position.index;
|
||||
}
|
||||
return -1;
|
||||
});
|
||||
|
||||
expect(tab1Positions).toEqual([0, 1]);
|
||||
});
|
||||
|
||||
it('should set moved widget position to end of destination tab', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const widgetA = makeWidget('widget-a', 0, 'tab-1');
|
||||
const widgetX = makeWidget('widget-x', 0, 'tab-2');
|
||||
const widgetY = makeWidget('widget-y', 1, 'tab-2');
|
||||
|
||||
store.set(
|
||||
getDraftAtom(),
|
||||
makeDraft([
|
||||
makeTab('tab-1', [widgetA], 0),
|
||||
makeTab('tab-2', [widgetX, widgetY], 1),
|
||||
]),
|
||||
);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useMoveWidgetToTab(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.moveWidgetToTab('widget-a', 'tab-2');
|
||||
});
|
||||
|
||||
const draft = store.get(getDraftAtom());
|
||||
const movedWidget = draft.tabs[1].widgets.find((w) => w.id === 'widget-a');
|
||||
|
||||
expect(movedWidget?.position).toEqual(
|
||||
expect.objectContaining({ index: 2 }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not change draft when source and destination are the same tab', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const widgetA = makeWidget('widget-a', 0, 'tab-1');
|
||||
const widgetB = makeWidget('widget-b', 1, 'tab-1');
|
||||
|
||||
const initialDraft = makeDraft([makeTab('tab-1', [widgetA, widgetB])]);
|
||||
store.set(getDraftAtom(), initialDraft);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useMoveWidgetToTab(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.moveWidgetToTab('widget-a', 'tab-1');
|
||||
});
|
||||
|
||||
const draft = store.get(getDraftAtom());
|
||||
|
||||
expect(draft).toBe(initialDraft);
|
||||
});
|
||||
|
||||
it('should not change draft when widget is not found', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const widgetA = makeWidget('widget-a', 0, 'tab-1');
|
||||
|
||||
const initialDraft = makeDraft([
|
||||
makeTab('tab-1', [widgetA], 0),
|
||||
makeTab('tab-2', [], 1),
|
||||
]);
|
||||
store.set(getDraftAtom(), initialDraft);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useMoveWidgetToTab(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.moveWidgetToTab('non-existent', 'tab-2');
|
||||
});
|
||||
|
||||
const draft = store.get(getDraftAtom());
|
||||
|
||||
expect(draft).toBe(initialDraft);
|
||||
});
|
||||
|
||||
it('should not change draft when destination tab is not found', () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const widgetA = makeWidget('widget-a', 0, 'tab-1');
|
||||
|
||||
const initialDraft = makeDraft([makeTab('tab-1', [widgetA])]);
|
||||
store.set(getDraftAtom(), initialDraft);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useMoveWidgetToTab(PAGE_LAYOUT_TEST_INSTANCE_ID),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.moveWidgetToTab('widget-a', 'non-existent-tab');
|
||||
});
|
||||
|
||||
const draft = store.get(getDraftAtom());
|
||||
|
||||
expect(draft).toBe(initialDraft);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { sortWidgetsByVerticalListPosition } from '@/page-layout/utils/sortWidgetsByVerticalListPosition';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { PageLayoutTabLayoutMode } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useCanMovePageLayoutWidgetDown = (
|
||||
pageLayoutIdFromProps?: string,
|
||||
) => {
|
||||
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
|
||||
PageLayoutComponentInstanceContext,
|
||||
pageLayoutIdFromProps,
|
||||
);
|
||||
|
||||
const pageLayoutDraftState = useAtomComponentStateCallbackState(
|
||||
pageLayoutDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const canMovePageLayoutWidgetDown = useCallback(
|
||||
(widgetId: string) => {
|
||||
const draft = store.get(pageLayoutDraftState);
|
||||
|
||||
const tab = draft.tabs.find((candidateTab) =>
|
||||
candidateTab.widgets.some((widget) => widget.id === widgetId),
|
||||
);
|
||||
|
||||
if (!tab || tab.layoutMode !== PageLayoutTabLayoutMode.VERTICAL_LIST) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const sortedWidgets = sortWidgetsByVerticalListPosition(tab.widgets);
|
||||
|
||||
const widgetIndex = sortedWidgets.findIndex(
|
||||
(widget) => widget.id === widgetId,
|
||||
);
|
||||
|
||||
return widgetIndex >= 0 && widgetIndex < sortedWidgets.length - 1;
|
||||
},
|
||||
[pageLayoutDraftState, store],
|
||||
);
|
||||
|
||||
return { canMovePageLayoutWidgetDown };
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { sortWidgetsByVerticalListPosition } from '@/page-layout/utils/sortWidgetsByVerticalListPosition';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { PageLayoutTabLayoutMode } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useCanMovePageLayoutWidgetUp = (
|
||||
pageLayoutIdFromProps?: string,
|
||||
) => {
|
||||
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
|
||||
PageLayoutComponentInstanceContext,
|
||||
pageLayoutIdFromProps,
|
||||
);
|
||||
|
||||
const pageLayoutDraftState = useAtomComponentStateCallbackState(
|
||||
pageLayoutDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const canMovePageLayoutWidgetUp = useCallback(
|
||||
(widgetId: string) => {
|
||||
const draft = store.get(pageLayoutDraftState);
|
||||
|
||||
const tab = draft.tabs.find((candidateTab) =>
|
||||
candidateTab.widgets.some((widget) => widget.id === widgetId),
|
||||
);
|
||||
|
||||
if (!tab || tab.layoutMode !== PageLayoutTabLayoutMode.VERTICAL_LIST) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const sortedWidgets = sortWidgetsByVerticalListPosition(tab.widgets);
|
||||
|
||||
const widgetIndex = sortedWidgets.findIndex(
|
||||
(widget) => widget.id === widgetId,
|
||||
);
|
||||
|
||||
return widgetIndex > 0;
|
||||
},
|
||||
[pageLayoutDraftState, store],
|
||||
);
|
||||
|
||||
return { canMovePageLayoutWidgetUp };
|
||||
};
|
||||
+9
-20
@@ -5,15 +5,14 @@ import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDr
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab';
|
||||
import { createDefaultFieldsWidget } from '@/page-layout/utils/createDefaultFieldsWidget';
|
||||
import { useCreateViewForFieldsWidget } from '@/page-layout/widgets/fields/hooks/useCreateViewForFieldsWidget';
|
||||
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
|
||||
import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { ViewType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useCreateRecordPageFieldsWidget = () => {
|
||||
const { tabId } = usePageLayoutContentContext();
|
||||
@@ -26,7 +25,7 @@ export const useCreateRecordPageFieldsWidget = () => {
|
||||
|
||||
const { currentPageLayout } = useCurrentPageLayoutOrThrow();
|
||||
|
||||
const { performViewAPICreate } = usePerformViewAPIPersist();
|
||||
const { createViewForFieldsWidget } = useCreateViewForFieldsWidget();
|
||||
|
||||
const pageLayoutDraftState = useAtomComponentStateCallbackState(
|
||||
pageLayoutDraftComponentState,
|
||||
@@ -41,22 +40,12 @@ export const useCreateRecordPageFieldsWidget = () => {
|
||||
const store = useStore();
|
||||
|
||||
const createRecordPageFieldsWidget = useCallback(async () => {
|
||||
const viewId = uuidv4();
|
||||
const viewId = await createViewForFieldsWidget({
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
viewName: `${objectMetadataItem.labelSingular} Fields`,
|
||||
});
|
||||
|
||||
const result = await performViewAPICreate(
|
||||
{
|
||||
input: {
|
||||
id: viewId,
|
||||
name: `${objectMetadataItem.labelSingular} Fields`,
|
||||
icon: 'IconList',
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
type: ViewType.FIELDS_WIDGET,
|
||||
},
|
||||
},
|
||||
objectMetadataItem.id,
|
||||
);
|
||||
|
||||
if (result.status === 'failed') {
|
||||
if (viewId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -81,18 +70,18 @@ export const useCreateRecordPageFieldsWidget = () => {
|
||||
store.set(pageLayoutEditingWidgetIdState, widgetId);
|
||||
|
||||
navigatePageLayoutSidePanel({
|
||||
sidePanelPage: SidePanelPages.PageLayoutFieldsSettings,
|
||||
sidePanelPage: SidePanelPages.RecordPageFieldsSettings,
|
||||
focusTitleInput: true,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
}, [
|
||||
createViewForFieldsWidget,
|
||||
currentPageLayout.tabs,
|
||||
navigatePageLayoutSidePanel,
|
||||
objectMetadataItem.id,
|
||||
objectMetadataItem.labelSingular,
|
||||
pageLayoutDraftState,
|
||||
pageLayoutEditingWidgetIdState,
|
||||
performViewAPICreate,
|
||||
store,
|
||||
tabId,
|
||||
]);
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
|
||||
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
|
||||
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { parseCellIdToCoordinates } from '@/page-layout/utils/parseCellIdToCoordinates';
|
||||
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
|
||||
export const useCreateWidgetFromClick = () => {
|
||||
export const useCreateWidgetFromClick = (pageLayoutIdFromProps?: string) => {
|
||||
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
|
||||
PageLayoutComponentInstanceContext,
|
||||
pageLayoutIdFromProps,
|
||||
);
|
||||
|
||||
const pageLayoutDraggedAreaState = useAtomComponentStateCallbackState(
|
||||
pageLayoutDraggedAreaComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const pageLayoutEditingWidgetIdState = useAtomComponentStateCallbackState(
|
||||
pageLayoutEditingWidgetIdComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const { navigatePageLayoutSidePanel } = useNavigatePageLayoutSidePanel();
|
||||
@@ -29,7 +38,7 @@ export const useCreateWidgetFromClick = () => {
|
||||
store.set(pageLayoutEditingWidgetIdState, null);
|
||||
|
||||
navigatePageLayoutSidePanel({
|
||||
sidePanelPage: SidePanelPages.PageLayoutWidgetTypeSelect,
|
||||
sidePanelPage: SidePanelPages.PageLayoutDashboardWidgetTypeSelect,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDr
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { removeWidgetFromTab } from '@/page-layout/utils/removeWidgetFromTab';
|
||||
import { removeWidgetLayoutFromTab } from '@/page-layout/utils/removeWidgetLayoutFromTab';
|
||||
import { useDeleteViewForFieldsWidget } from '@/page-layout/widgets/fields/hooks/useDeleteViewForFieldsWidget';
|
||||
import { useDeleteViewForRecordTableWidget } from '@/page-layout/widgets/record-table/hooks/useDeleteViewForRecordTableWidget';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
@@ -36,6 +37,8 @@ export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
|
||||
|
||||
const { closeSidePanelMenu } = useSidePanelMenu();
|
||||
|
||||
const { deleteViewForFieldsWidget } = useDeleteViewForFieldsWidget();
|
||||
|
||||
const { deleteViewForRecordTableWidget } =
|
||||
useDeleteViewForRecordTableWidget();
|
||||
|
||||
@@ -67,6 +70,17 @@ export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(widgetToDelete) &&
|
||||
widgetToDelete.type === WidgetType.FIELDS &&
|
||||
'viewId' in widgetToDelete.configuration &&
|
||||
isDefined(widgetToDelete.configuration.viewId)
|
||||
) {
|
||||
deleteViewForFieldsWidget(
|
||||
widgetToDelete.configuration.viewId as string,
|
||||
);
|
||||
}
|
||||
|
||||
const tabId = tabWithWidget?.id;
|
||||
|
||||
if (isDefined(tabId)) {
|
||||
@@ -93,6 +107,7 @@ export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
|
||||
},
|
||||
[
|
||||
closeSidePanelMenu,
|
||||
deleteViewForFieldsWidget,
|
||||
deleteViewForRecordTableWidget,
|
||||
pageLayoutCurrentLayoutsState,
|
||||
pageLayoutDraftState,
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
|
||||
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
|
||||
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { pageLayoutSelectedCellsComponentState } from '@/page-layout/states/pageLayoutSelectedCellsComponentState';
|
||||
import { calculateGridBoundsFromSelectedCells } from '@/page-layout/utils/calculateGridBoundsFromSelectedCells';
|
||||
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useStore } from 'jotai';
|
||||
@@ -50,7 +50,7 @@ export const useEndPageLayoutDragSelection = (
|
||||
store.set(pageLayoutEditingWidgetIdState, null);
|
||||
|
||||
navigatePageLayoutSidePanel({
|
||||
sidePanelPage: SidePanelPages.PageLayoutWidgetTypeSelect,
|
||||
sidePanelPage: SidePanelPages.PageLayoutDashboardWidgetTypeSelect,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
}
|
||||
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { widgetInsertionContextComponentState } from '@/page-layout/states/widgetInsertionContextComponentState';
|
||||
import { isVerticalListPosition } from '@/page-layout/utils/isVerticalListPosition';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { PageLayoutTabLayoutMode } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useInsertCreatedWidgetAtContext = (
|
||||
pageLayoutIdFromProps?: string,
|
||||
) => {
|
||||
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
|
||||
PageLayoutComponentInstanceContext,
|
||||
pageLayoutIdFromProps,
|
||||
);
|
||||
|
||||
const pageLayoutDraftState = useAtomComponentStateCallbackState(
|
||||
pageLayoutDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const widgetInsertionContextState = useAtomComponentStateCallbackState(
|
||||
widgetInsertionContextComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const insertCreatedWidgetAtContext = useCallback(
|
||||
(newWidgetId: string) => {
|
||||
const insertionContext = store.get(widgetInsertionContextState);
|
||||
|
||||
if (insertionContext === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
store.set(pageLayoutDraftState, (prev) => {
|
||||
const tab = prev.tabs.find((candidateTab) =>
|
||||
candidateTab.widgets.some(
|
||||
(widget) => widget.id === insertionContext.targetWidgetId,
|
||||
),
|
||||
);
|
||||
|
||||
if (!tab) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const sortedWidgets = tab.widgets.toSorted((widgetA, widgetB) => {
|
||||
const indexA =
|
||||
isDefined(widgetA.position) &&
|
||||
isVerticalListPosition(widgetA.position)
|
||||
? widgetA.position.index
|
||||
: 0;
|
||||
const indexB =
|
||||
isDefined(widgetB.position) &&
|
||||
isVerticalListPosition(widgetB.position)
|
||||
? widgetB.position.index
|
||||
: 0;
|
||||
return indexA - indexB;
|
||||
});
|
||||
|
||||
const targetIndex = sortedWidgets.findIndex(
|
||||
(widget) => widget.id === insertionContext.targetWidgetId,
|
||||
);
|
||||
|
||||
if (targetIndex < 0) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const insertAtIndex =
|
||||
insertionContext.direction === 'above'
|
||||
? targetIndex
|
||||
: targetIndex + 1;
|
||||
|
||||
const widgetsWithoutNew = sortedWidgets.filter(
|
||||
(widget) => widget.id !== newWidgetId,
|
||||
);
|
||||
|
||||
const newWidget = tab.widgets.find(
|
||||
(widget) => widget.id === newWidgetId,
|
||||
);
|
||||
|
||||
if (!newWidget) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
widgetsWithoutNew.splice(insertAtIndex, 0, newWidget);
|
||||
|
||||
const reindexedWidgets = widgetsWithoutNew.map(
|
||||
(widget, widgetIndex) => ({
|
||||
...widget,
|
||||
position: {
|
||||
__typename: 'PageLayoutWidgetVerticalListPosition' as const,
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
index: widgetIndex,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
...prev,
|
||||
tabs: prev.tabs.map((currentTab) => {
|
||||
if (currentTab.id !== tab.id) {
|
||||
return currentTab;
|
||||
}
|
||||
return {
|
||||
...currentTab,
|
||||
widgets: reindexedWidgets,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
store.set(widgetInsertionContextState, null);
|
||||
},
|
||||
[pageLayoutDraftState, store, widgetInsertionContextState],
|
||||
);
|
||||
|
||||
return { insertCreatedWidgetAtContext };
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { isVerticalListPosition } from '@/page-layout/utils/isVerticalListPosition';
|
||||
import { sortWidgetsByVerticalListPosition } from '@/page-layout/utils/sortWidgetsByVerticalListPosition';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { PageLayoutTabLayoutMode } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useMovePageLayoutWidgetDown = (pageLayoutIdFromProps?: string) => {
|
||||
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
|
||||
PageLayoutComponentInstanceContext,
|
||||
pageLayoutIdFromProps,
|
||||
);
|
||||
|
||||
const pageLayoutDraftState = useAtomComponentStateCallbackState(
|
||||
pageLayoutDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const movePageLayoutWidgetDown = useCallback(
|
||||
(widgetId: string) => {
|
||||
store.set(pageLayoutDraftState, (prev) => {
|
||||
const tab = prev.tabs.find((candidateTab) =>
|
||||
candidateTab.widgets.some((widget) => widget.id === widgetId),
|
||||
);
|
||||
|
||||
if (!tab) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const sortedWidgets = sortWidgetsByVerticalListPosition(tab.widgets);
|
||||
|
||||
const currentIndex = sortedWidgets.findIndex(
|
||||
(widget) => widget.id === widgetId,
|
||||
);
|
||||
|
||||
if (currentIndex < 0 || currentIndex >= sortedWidgets.length - 1) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const currentWidget = sortedWidgets[currentIndex];
|
||||
const neighborWidget = sortedWidgets[currentIndex + 1];
|
||||
|
||||
const currentPositionIndex =
|
||||
isDefined(currentWidget.position) &&
|
||||
isVerticalListPosition(currentWidget.position)
|
||||
? currentWidget.position.index
|
||||
: currentIndex;
|
||||
const neighborPositionIndex =
|
||||
isDefined(neighborWidget.position) &&
|
||||
isVerticalListPosition(neighborWidget.position)
|
||||
? neighborWidget.position.index
|
||||
: currentIndex + 1;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
tabs: prev.tabs.map((currentTab) => {
|
||||
if (currentTab.id !== tab.id) {
|
||||
return currentTab;
|
||||
}
|
||||
return {
|
||||
...currentTab,
|
||||
widgets: currentTab.widgets.map((widget) => {
|
||||
if (widget.id === currentWidget.id) {
|
||||
return {
|
||||
...widget,
|
||||
position: {
|
||||
__typename:
|
||||
'PageLayoutWidgetVerticalListPosition' as const,
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
index: neighborPositionIndex,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (widget.id === neighborWidget.id) {
|
||||
return {
|
||||
...widget,
|
||||
position: {
|
||||
__typename:
|
||||
'PageLayoutWidgetVerticalListPosition' as const,
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
index: currentPositionIndex,
|
||||
},
|
||||
};
|
||||
}
|
||||
return widget;
|
||||
}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
},
|
||||
[pageLayoutDraftState, store],
|
||||
);
|
||||
|
||||
return { movePageLayoutWidgetDown };
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { isVerticalListPosition } from '@/page-layout/utils/isVerticalListPosition';
|
||||
import { sortWidgetsByVerticalListPosition } from '@/page-layout/utils/sortWidgetsByVerticalListPosition';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { PageLayoutTabLayoutMode } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useMovePageLayoutWidgetUp = (pageLayoutIdFromProps?: string) => {
|
||||
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
|
||||
PageLayoutComponentInstanceContext,
|
||||
pageLayoutIdFromProps,
|
||||
);
|
||||
|
||||
const pageLayoutDraftState = useAtomComponentStateCallbackState(
|
||||
pageLayoutDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const movePageLayoutWidgetUp = useCallback(
|
||||
(widgetId: string) => {
|
||||
store.set(pageLayoutDraftState, (prev) => {
|
||||
const tab = prev.tabs.find((candidateTab) =>
|
||||
candidateTab.widgets.some((widget) => widget.id === widgetId),
|
||||
);
|
||||
|
||||
if (!tab) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const sortedWidgets = sortWidgetsByVerticalListPosition(tab.widgets);
|
||||
|
||||
const currentIndex = sortedWidgets.findIndex(
|
||||
(widget) => widget.id === widgetId,
|
||||
);
|
||||
|
||||
if (currentIndex <= 0) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const currentWidget = sortedWidgets[currentIndex];
|
||||
const neighborWidget = sortedWidgets[currentIndex - 1];
|
||||
|
||||
const currentPositionIndex =
|
||||
isDefined(currentWidget.position) &&
|
||||
isVerticalListPosition(currentWidget.position)
|
||||
? currentWidget.position.index
|
||||
: currentIndex;
|
||||
const neighborPositionIndex =
|
||||
isDefined(neighborWidget.position) &&
|
||||
isVerticalListPosition(neighborWidget.position)
|
||||
? neighborWidget.position.index
|
||||
: currentIndex - 1;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
tabs: prev.tabs.map((currentTab) => {
|
||||
if (currentTab.id !== tab.id) {
|
||||
return currentTab;
|
||||
}
|
||||
return {
|
||||
...currentTab,
|
||||
widgets: currentTab.widgets.map((widget) => {
|
||||
if (widget.id === currentWidget.id) {
|
||||
return {
|
||||
...widget,
|
||||
position: {
|
||||
__typename:
|
||||
'PageLayoutWidgetVerticalListPosition' as const,
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
index: neighborPositionIndex,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (widget.id === neighborWidget.id) {
|
||||
return {
|
||||
...widget,
|
||||
position: {
|
||||
__typename:
|
||||
'PageLayoutWidgetVerticalListPosition' as const,
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
index: currentPositionIndex,
|
||||
},
|
||||
};
|
||||
}
|
||||
return widget;
|
||||
}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
},
|
||||
[pageLayoutDraftState, store],
|
||||
);
|
||||
|
||||
return { movePageLayoutWidgetUp };
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { isVerticalListPosition } from '@/page-layout/utils/isVerticalListPosition';
|
||||
import { sortWidgetsByVerticalListPosition } from '@/page-layout/utils/sortWidgetsByVerticalListPosition';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { PageLayoutTabLayoutMode } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useMoveWidgetToTab = (pageLayoutIdFromProps?: string) => {
|
||||
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
|
||||
PageLayoutComponentInstanceContext,
|
||||
pageLayoutIdFromProps,
|
||||
);
|
||||
|
||||
const pageLayoutDraftState = useAtomComponentStateCallbackState(
|
||||
pageLayoutDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const moveWidgetToTab = useCallback(
|
||||
(widgetId: string, destinationTabId: string) => {
|
||||
store.set(pageLayoutDraftState, (prev) => {
|
||||
const sourceTab = prev.tabs.find((candidateTab) =>
|
||||
candidateTab.widgets.some((widget) => widget.id === widgetId),
|
||||
);
|
||||
|
||||
if (!sourceTab) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
if (sourceTab.id === destinationTabId) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const destinationTab = prev.tabs.find(
|
||||
(candidateTab) => candidateTab.id === destinationTabId,
|
||||
);
|
||||
|
||||
if (!destinationTab) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const widget = sourceTab.widgets.find(
|
||||
(candidateWidget) => candidateWidget.id === widgetId,
|
||||
);
|
||||
|
||||
if (!widget) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const movedWidget = {
|
||||
...widget,
|
||||
pageLayoutTabId: destinationTabId,
|
||||
position: {
|
||||
__typename: 'PageLayoutWidgetVerticalListPosition' as const,
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
index: destinationTab.widgets.length,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
...prev,
|
||||
tabs: prev.tabs.map((currentTab) => {
|
||||
if (currentTab.id === sourceTab.id) {
|
||||
const remainingWidgets = sortWidgetsByVerticalListPosition(
|
||||
currentTab.widgets,
|
||||
)
|
||||
.filter((tabWidget) => tabWidget.id !== widgetId)
|
||||
.map((tabWidget, widgetIndex) => ({
|
||||
...tabWidget,
|
||||
position:
|
||||
isDefined(tabWidget.position) &&
|
||||
isVerticalListPosition(tabWidget.position)
|
||||
? {
|
||||
__typename:
|
||||
'PageLayoutWidgetVerticalListPosition' as const,
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
index: widgetIndex,
|
||||
}
|
||||
: tabWidget.position,
|
||||
}));
|
||||
|
||||
return {
|
||||
...currentTab,
|
||||
widgets: remainingWidgets,
|
||||
};
|
||||
}
|
||||
if (currentTab.id === destinationTabId) {
|
||||
return {
|
||||
...currentTab,
|
||||
widgets: [...currentTab.widgets, movedWidget],
|
||||
};
|
||||
}
|
||||
return currentTab;
|
||||
}),
|
||||
};
|
||||
});
|
||||
},
|
||||
[pageLayoutDraftState, store],
|
||||
);
|
||||
|
||||
return { moveWidgetToTab };
|
||||
};
|
||||
@@ -7,7 +7,7 @@ export const useNavigateToMoreWidgets = () => {
|
||||
|
||||
const navigateToMoreWidgets = useCallback(() => {
|
||||
navigatePageLayoutSidePanel({
|
||||
sidePanelPage: SidePanelPages.PageLayoutWidgetTypeSelect,
|
||||
sidePanelPage: SidePanelPages.PageLayoutRecordPageWidgetTypeSelect,
|
||||
});
|
||||
}, [navigatePageLayoutSidePanel]);
|
||||
|
||||
|
||||
+18
-2
@@ -3,9 +3,11 @@ import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutIn
|
||||
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
|
||||
import { buildWidgetVisibilityContext } from '@/page-layout/utils/buildWidgetVisibilityContext';
|
||||
import { filterVisibleWidgets } from '@/page-layout/utils/filterVisibleWidgets';
|
||||
import { sortWidgetsByVerticalListPosition } from '@/page-layout/utils/sortWidgetsByVerticalListPosition';
|
||||
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { PageLayoutTabLayoutMode } from '~/generated-metadata/graphql';
|
||||
|
||||
export const usePageLayoutTabWithVisibleWidgetsOrThrow = (
|
||||
tabId: string,
|
||||
@@ -26,13 +28,27 @@ export const usePageLayoutTabWithVisibleWidgetsOrThrow = (
|
||||
}
|
||||
|
||||
if (isPageLayoutInEditMode) {
|
||||
return tab;
|
||||
return {
|
||||
...tab,
|
||||
widgets:
|
||||
tab.layoutMode === PageLayoutTabLayoutMode.VERTICAL_LIST
|
||||
? sortWidgetsByVerticalListPosition(tab.widgets)
|
||||
: tab.widgets,
|
||||
};
|
||||
}
|
||||
|
||||
const context = buildWidgetVisibilityContext({ isMobile, isInSidePanel });
|
||||
|
||||
const visibleWidgets = filterVisibleWidgets({
|
||||
widgets: tab.widgets,
|
||||
context,
|
||||
});
|
||||
|
||||
return {
|
||||
...tab,
|
||||
widgets: filterVisibleWidgets({ widgets: tab.widgets, context }),
|
||||
widgets:
|
||||
tab.layoutMode === PageLayoutTabLayoutMode.VERTICAL_LIST
|
||||
? sortWidgetsByVerticalListPosition(visibleWidgets)
|
||||
: visibleWidgets,
|
||||
};
|
||||
};
|
||||
|
||||
+18
-8
@@ -3,8 +3,10 @@ import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pag
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { getWidgetConfigurationViewId } from '@/page-layout/utils/getWidgetConfigurationViewId';
|
||||
import { removeWidgetFromTab } from '@/page-layout/utils/removeWidgetFromTab';
|
||||
import { removeWidgetLayoutFromTab } from '@/page-layout/utils/removeWidgetLayoutFromTab';
|
||||
import { useDeleteViewForFieldsWidget } from '@/page-layout/widgets/fields/hooks/useDeleteViewForFieldsWidget';
|
||||
import { useDeleteViewForRecordTableWidget } from '@/page-layout/widgets/record-table/hooks/useDeleteViewForRecordTableWidget';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
@@ -41,6 +43,8 @@ export const useRemovePageLayoutWidgetAndPreservePosition = (
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const { deleteViewForFieldsWidget } = useDeleteViewForFieldsWidget();
|
||||
|
||||
const { deleteViewForRecordTableWidget } =
|
||||
useDeleteViewForRecordTableWidget();
|
||||
|
||||
@@ -59,15 +63,20 @@ export const useRemovePageLayoutWidgetAndPreservePosition = (
|
||||
(widget) => widget.id === widgetId,
|
||||
);
|
||||
|
||||
if (
|
||||
isDefined(widgetToRemove) &&
|
||||
widgetToRemove.type === WidgetType.RECORD_TABLE &&
|
||||
'viewId' in widgetToRemove.configuration &&
|
||||
isDefined(widgetToRemove.configuration.viewId)
|
||||
) {
|
||||
deleteViewForRecordTableWidget(
|
||||
widgetToRemove.configuration.viewId as string,
|
||||
if (isDefined(widgetToRemove)) {
|
||||
const viewId = getWidgetConfigurationViewId(
|
||||
widgetToRemove.configuration,
|
||||
);
|
||||
|
||||
if (isDefined(viewId)) {
|
||||
if (widgetToRemove.type === WidgetType.RECORD_TABLE) {
|
||||
deleteViewForRecordTableWidget(viewId);
|
||||
}
|
||||
|
||||
if (widgetToRemove.type === WidgetType.FIELDS) {
|
||||
deleteViewForFieldsWidget(viewId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const tabId = tabWithWidget?.id;
|
||||
@@ -107,6 +116,7 @@ export const useRemovePageLayoutWidgetAndPreservePosition = (
|
||||
store.set(pageLayoutEditingWidgetIdState, null);
|
||||
},
|
||||
[
|
||||
deleteViewForFieldsWidget,
|
||||
deleteViewForRecordTableWidget,
|
||||
pageLayoutCurrentLayoutsState,
|
||||
pageLayoutDraftState,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { type DropResult } from '@hello-pangea/dnd';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { PageLayoutTabLayoutMode } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useReorderPageLayoutWidgets = (
|
||||
tabId: string,
|
||||
@@ -35,10 +36,19 @@ export const useReorderPageLayoutWidgets = (
|
||||
const [removed] = newWidgets.splice(result.source.index, 1);
|
||||
newWidgets.splice(result.destination!.index, 0, removed);
|
||||
|
||||
const reindexedWidgets = newWidgets.map((widget, index) => ({
|
||||
...widget,
|
||||
position: {
|
||||
__typename: 'PageLayoutWidgetVerticalListPosition' as const,
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
index,
|
||||
},
|
||||
}));
|
||||
|
||||
return {
|
||||
...prev,
|
||||
tabs: prev.tabs.map((t) =>
|
||||
t.id === tabId ? { ...t, widgets: newWidgets } : t,
|
||||
t.id === tabId ? { ...t, widgets: reindexedWidgets } : t,
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
|
||||
|
||||
import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext';
|
||||
|
||||
export type WidgetInsertionContext = {
|
||||
targetWidgetId: string;
|
||||
direction: 'above' | 'below';
|
||||
} | null;
|
||||
|
||||
export const widgetInsertionContextComponentState =
|
||||
createAtomComponentState<WidgetInsertionContext>({
|
||||
key: 'widgetInsertionContextComponentState',
|
||||
defaultValue: null,
|
||||
componentInstanceContext: PageLayoutComponentInstanceContext,
|
||||
});
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { getWidgetConfigurationViewId } from '@/page-layout/utils/getWidgetConfigurationViewId';
|
||||
|
||||
describe('getWidgetConfigurationViewId', () => {
|
||||
it('should return viewId when present and is a string', () => {
|
||||
const configuration = {
|
||||
__typename: 'RecordTableConfiguration',
|
||||
configurationType: 'RECORD_TABLE',
|
||||
viewId: 'view-123',
|
||||
} as unknown as PageLayoutWidget['configuration'];
|
||||
|
||||
expect(getWidgetConfigurationViewId(configuration)).toBe('view-123');
|
||||
});
|
||||
|
||||
it('should return null when viewId is not present', () => {
|
||||
const configuration = {
|
||||
__typename: 'IframeConfiguration',
|
||||
configurationType: 'IFRAME',
|
||||
url: 'https://example.com',
|
||||
} as unknown as PageLayoutWidget['configuration'];
|
||||
|
||||
expect(getWidgetConfigurationViewId(configuration)).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when viewId is not a string', () => {
|
||||
const configuration = {
|
||||
__typename: 'RecordTableConfiguration',
|
||||
configurationType: 'RECORD_TABLE',
|
||||
viewId: 123,
|
||||
} as unknown as PageLayoutWidget['configuration'];
|
||||
|
||||
expect(getWidgetConfigurationViewId(configuration)).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when viewId is null', () => {
|
||||
const configuration = {
|
||||
__typename: 'RecordTableConfiguration',
|
||||
configurationType: 'RECORD_TABLE',
|
||||
viewId: null,
|
||||
} as unknown as PageLayoutWidget['configuration'];
|
||||
|
||||
expect(getWidgetConfigurationViewId(configuration)).toBeNull();
|
||||
});
|
||||
});
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { isVerticalListPosition } from '@/page-layout/utils/isVerticalListPosition';
|
||||
import {
|
||||
type PageLayoutWidgetPosition,
|
||||
PageLayoutTabLayoutMode,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
describe('isVerticalListPosition', () => {
|
||||
it('should return true for VERTICAL_LIST layout mode', () => {
|
||||
const position: PageLayoutWidgetPosition = {
|
||||
__typename: 'PageLayoutWidgetVerticalListPosition',
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
index: 0,
|
||||
};
|
||||
|
||||
expect(isVerticalListPosition(position)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for CANVAS layout mode', () => {
|
||||
const position: PageLayoutWidgetPosition = {
|
||||
__typename: 'PageLayoutWidgetCanvasPosition',
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||||
} as PageLayoutWidgetPosition;
|
||||
|
||||
expect(isVerticalListPosition(position)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for GRID layout mode', () => {
|
||||
const position: PageLayoutWidgetPosition = {
|
||||
__typename: 'PageLayoutWidgetGridPosition',
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
} as PageLayoutWidgetPosition;
|
||||
|
||||
expect(isVerticalListPosition(position)).toBe(false);
|
||||
});
|
||||
});
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { sortWidgetsByVerticalListPosition } from '@/page-layout/utils/sortWidgetsByVerticalListPosition';
|
||||
import {
|
||||
PageLayoutTabLayoutMode,
|
||||
WidgetConfigurationType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const makeWidget = (id: string, index: number): PageLayoutWidget =>
|
||||
({
|
||||
__typename: 'PageLayoutWidget',
|
||||
id,
|
||||
pageLayoutTabId: 'tab-1',
|
||||
title: id,
|
||||
type: WidgetType.FIELDS,
|
||||
isOverridden: false,
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 1, columnSpan: 1 },
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
},
|
||||
position: {
|
||||
__typename: 'PageLayoutWidgetVerticalListPosition',
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
index,
|
||||
},
|
||||
objectMetadataId: null,
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
deletedAt: null,
|
||||
}) as unknown as PageLayoutWidget;
|
||||
|
||||
describe('sortWidgetsByVerticalListPosition', () => {
|
||||
it('should sort widgets by index ascending', () => {
|
||||
const widgets = [
|
||||
makeWidget('c', 2),
|
||||
makeWidget('a', 0),
|
||||
makeWidget('b', 1),
|
||||
];
|
||||
|
||||
const sorted = sortWidgetsByVerticalListPosition(widgets);
|
||||
|
||||
expect(sorted.map((w) => w.id)).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
it('should not mutate the original array', () => {
|
||||
const widgets = [makeWidget('b', 1), makeWidget('a', 0)];
|
||||
const original = [...widgets];
|
||||
|
||||
sortWidgetsByVerticalListPosition(widgets);
|
||||
|
||||
expect(widgets.map((w) => w.id)).toEqual(original.map((w) => w.id));
|
||||
});
|
||||
|
||||
it('should handle widgets with undefined positions by treating as index 0', () => {
|
||||
const widgetWithPosition = makeWidget('b', 1);
|
||||
const widgetWithoutPosition = {
|
||||
...makeWidget('a', 0),
|
||||
position: undefined,
|
||||
} as unknown as PageLayoutWidget;
|
||||
|
||||
const sorted = sortWidgetsByVerticalListPosition([
|
||||
widgetWithPosition,
|
||||
widgetWithoutPosition,
|
||||
]);
|
||||
|
||||
expect(sorted.map((w) => w.id)).toEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
it('should return empty array when given empty array', () => {
|
||||
expect(sortWidgetsByVerticalListPosition([])).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle single widget', () => {
|
||||
const widgets = [makeWidget('a', 0)];
|
||||
|
||||
const sorted = sortWidgetsByVerticalListPosition(widgets);
|
||||
|
||||
expect(sorted.map((w) => w.id)).toEqual(['a']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
|
||||
export const getWidgetConfigurationViewId = (
|
||||
configuration: PageLayoutWidget['configuration'],
|
||||
): string | null => {
|
||||
if ('viewId' in configuration && typeof configuration.viewId === 'string') {
|
||||
return configuration.viewId;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import {
|
||||
type PageLayoutWidgetPosition,
|
||||
type PageLayoutWidgetVerticalListPosition,
|
||||
PageLayoutTabLayoutMode,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const isVerticalListPosition = (
|
||||
position: PageLayoutWidgetPosition,
|
||||
): position is PageLayoutWidgetVerticalListPosition =>
|
||||
position.layoutMode === PageLayoutTabLayoutMode.VERTICAL_LIST;
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { isVerticalListPosition } from '@/page-layout/utils/isVerticalListPosition';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const sortWidgetsByVerticalListPosition = (
|
||||
widgets: PageLayoutWidget[],
|
||||
): PageLayoutWidget[] =>
|
||||
[...widgets].sort((widgetA, widgetB) => {
|
||||
const indexA =
|
||||
isDefined(widgetA.position) && isVerticalListPosition(widgetA.position)
|
||||
? widgetA.position.index
|
||||
: 0;
|
||||
const indexB =
|
||||
isDefined(widgetB.position) && isVerticalListPosition(widgetB.position)
|
||||
? widgetB.position.index
|
||||
: 0;
|
||||
return indexA - indexB;
|
||||
});
|
||||
+1
-1
@@ -43,7 +43,7 @@ export const DashboardWidgetPlaceholder = () => {
|
||||
setIsPageLayoutInEditMode(true);
|
||||
}
|
||||
navigatePageLayoutSidePanel({
|
||||
sidePanelPage: SidePanelPages.PageLayoutWidgetTypeSelect,
|
||||
sidePanelPage: SidePanelPages.PageLayoutDashboardWidgetTypeSelect,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { usePageLayoutContentContext } from '@/page-layout/contexts/PageLayoutContentContext';
|
||||
import { useCurrentPageLayoutOrThrow } from '@/page-layout/hooks/useCurrentPageLayoutOrThrow';
|
||||
import { useDeletePageLayoutWidget } from '@/page-layout/hooks/useDeletePageLayoutWidget';
|
||||
import { useEditPageLayoutWidget } from '@/page-layout/hooks/useEditPageLayoutWidget';
|
||||
import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
|
||||
import { pageLayoutDraggingWidgetIdComponentState } from '@/page-layout/states/pageLayoutDraggingWidgetIdComponentState';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
@@ -20,6 +19,7 @@ import { getWidgetCardVariant } from '@/page-layout/widgets/utils/getWidgetCardV
|
||||
import { WidgetCard } from '@/page-layout/widgets/widget-card/components/WidgetCard';
|
||||
import { WidgetCardContent } from '@/page-layout/widgets/widget-card/components/WidgetCardContent';
|
||||
import { WidgetCardHeader } from '@/page-layout/widgets/widget-card/components/WidgetCardHeader';
|
||||
import { useOpenWidgetSettingsInSidePanel } from '@/side-panel/hooks/useOpenWidgetSettingsInSidePanel';
|
||||
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
@@ -54,7 +54,7 @@ type WidgetRendererProps = {
|
||||
export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { deletePageLayoutWidget } = useDeletePageLayoutWidget();
|
||||
const { handleEditWidget } = useEditPageLayoutWidget();
|
||||
const { openWidgetSettingsInSidePanel } = useOpenWidgetSettingsInSidePanel();
|
||||
|
||||
const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
|
||||
|
||||
@@ -121,7 +121,7 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
|
||||
layoutMode !== PageLayoutTabLayoutMode.CANVAS && !hideHeaderInViewMode;
|
||||
|
||||
const handleClick = () => {
|
||||
handleEditWidget({
|
||||
openWidgetSettingsInSidePanel({
|
||||
widgetId: widget.id,
|
||||
widgetType: widget.type,
|
||||
});
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist';
|
||||
import { useCallback } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { ViewType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useCreateViewForFieldsWidget = () => {
|
||||
const { performViewAPICreate } = usePerformViewAPIPersist();
|
||||
|
||||
const createViewForFieldsWidget = useCallback(
|
||||
async ({
|
||||
objectMetadataId,
|
||||
viewName,
|
||||
}: {
|
||||
objectMetadataId: string;
|
||||
viewName: string;
|
||||
}) => {
|
||||
const viewId = uuidv4();
|
||||
|
||||
const result = await performViewAPICreate(
|
||||
{
|
||||
input: {
|
||||
id: viewId,
|
||||
name: viewName,
|
||||
icon: 'IconList',
|
||||
objectMetadataId,
|
||||
type: ViewType.FIELDS_WIDGET,
|
||||
},
|
||||
},
|
||||
objectMetadataId,
|
||||
);
|
||||
|
||||
if (result.status === 'failed') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return viewId;
|
||||
},
|
||||
[performViewAPICreate],
|
||||
);
|
||||
|
||||
return { createViewForFieldsWidget };
|
||||
};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useDeleteViewForFieldsWidget = () => {
|
||||
const { performViewAPIDestroy } = usePerformViewAPIPersist();
|
||||
|
||||
const deleteViewForFieldsWidget = useCallback(
|
||||
async (viewId: string) => {
|
||||
await performViewAPIDestroy({ id: viewId });
|
||||
},
|
||||
[performViewAPIDestroy],
|
||||
);
|
||||
|
||||
return { deleteViewForFieldsWidget };
|
||||
};
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledFormContainer = styled.div`
|
||||
padding-inline: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
export const SidePanelGroupFormContainer = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
return <StyledFormContainer>{children}</StyledFormContainer>;
|
||||
};
|
||||
@@ -16,6 +16,7 @@ import { SidePanelPageInfoLayout } from '@/side-panel/components/SidePanelPageIn
|
||||
import { SidePanelPageLayoutInfo } from '@/side-panel/components/SidePanelPageLayoutInfo';
|
||||
import { SidePanelRecordInfo } from '@/side-panel/components/SidePanelRecordInfo';
|
||||
import { SidePanelWorkflowStepInfo } from '@/side-panel/components/SidePanelWorkflowStepInfo';
|
||||
import { isPageLayoutSidePanelPage } from '@/side-panel/pages/page-layout/utils/isPageLayoutSidePanelPage';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { NavigationMenuItemType, SidePanelPages } from 'twenty-shared/types';
|
||||
|
||||
@@ -95,15 +96,7 @@ export const SidePanelPageInfo = ({ pageChip }: SidePanelPageInfoProps) => {
|
||||
}
|
||||
|
||||
const isPageLayoutPage = pageChip.page?.page
|
||||
? [
|
||||
SidePanelPages.PageLayoutWidgetTypeSelect,
|
||||
SidePanelPages.PageLayoutGraphTypeSelect,
|
||||
SidePanelPages.PageLayoutIframeSettings,
|
||||
SidePanelPages.PageLayoutTabSettings,
|
||||
SidePanelPages.PageLayoutFieldsSettings,
|
||||
SidePanelPages.PageLayoutFieldSettings,
|
||||
SidePanelPages.PageLayoutRecordTableSettings,
|
||||
].includes(pageChip.page?.page)
|
||||
? isPageLayoutSidePanelPage(pageChip.page.page)
|
||||
: false;
|
||||
|
||||
if (isPageLayoutPage) {
|
||||
|
||||
+18
-6
@@ -76,7 +76,7 @@ export const usePageLayoutHeaderInfo = ({
|
||||
};
|
||||
}
|
||||
|
||||
case SidePanelPages.PageLayoutIframeSettings: {
|
||||
case SidePanelPages.DashboardIframeSettings: {
|
||||
if (!isDefined(pageLayoutEditingWidgetId)) {
|
||||
return null;
|
||||
}
|
||||
@@ -106,7 +106,7 @@ export const usePageLayoutHeaderInfo = ({
|
||||
};
|
||||
}
|
||||
|
||||
case SidePanelPages.PageLayoutGraphTypeSelect: {
|
||||
case SidePanelPages.DashboardChartSettings: {
|
||||
if (!isDefined(pageLayoutEditingWidgetId)) {
|
||||
return null;
|
||||
}
|
||||
@@ -145,7 +145,7 @@ export const usePageLayoutHeaderInfo = ({
|
||||
};
|
||||
}
|
||||
|
||||
case SidePanelPages.PageLayoutFieldsSettings: {
|
||||
case SidePanelPages.RecordPageFieldsSettings: {
|
||||
if (!isDefined(pageLayoutEditingWidgetId)) {
|
||||
return null;
|
||||
}
|
||||
@@ -175,7 +175,7 @@ export const usePageLayoutHeaderInfo = ({
|
||||
};
|
||||
}
|
||||
|
||||
case SidePanelPages.PageLayoutFieldSettings: {
|
||||
case SidePanelPages.RecordPageFieldSettings: {
|
||||
if (!isDefined(pageLayoutEditingWidgetId)) {
|
||||
return null;
|
||||
}
|
||||
@@ -205,7 +205,7 @@ export const usePageLayoutHeaderInfo = ({
|
||||
};
|
||||
}
|
||||
|
||||
case SidePanelPages.PageLayoutRecordTableSettings: {
|
||||
case SidePanelPages.DashboardRecordTableSettings: {
|
||||
if (!isDefined(pageLayoutEditingWidgetId)) {
|
||||
return null;
|
||||
}
|
||||
@@ -235,7 +235,19 @@ export const usePageLayoutHeaderInfo = ({
|
||||
};
|
||||
}
|
||||
|
||||
case SidePanelPages.PageLayoutWidgetTypeSelect: {
|
||||
case SidePanelPages.PageLayoutDashboardWidgetTypeSelect: {
|
||||
return {
|
||||
headerIcon: IconPlus,
|
||||
headerIconColor: iconColor,
|
||||
headerType: '',
|
||||
title: t`New widget`,
|
||||
isReadonly: true,
|
||||
tab: undefined,
|
||||
widgetInEditMode: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
case SidePanelPages.PageLayoutRecordPageWidgetTypeSelect: {
|
||||
return {
|
||||
headerIcon: IconPlus,
|
||||
headerIconColor: iconColor,
|
||||
|
||||
@@ -7,13 +7,14 @@ import { SidePanelAskAIPage } from '@/side-panel/pages/ask-ai/components/SidePan
|
||||
import { SidePanelCalendarEventPage } from '@/side-panel/pages/calendar-event/components/SidePanelCalendarEventPage';
|
||||
import { SidePanelComposeEmailPage } from '@/side-panel/pages/compose-email/components/SidePanelComposeEmailPage';
|
||||
import { SidePanelFrontComponentPage } from '@/side-panel/pages/front-component/components/SidePanelFrontComponentPage';
|
||||
import { SidePanelPageLayoutChartSettings } from '@/side-panel/pages/page-layout/components/SidePanelPageLayoutChartSettings';
|
||||
import { SidePanelPageLayoutFieldSettings } from '@/side-panel/pages/page-layout/components/SidePanelPageLayoutFieldSettings';
|
||||
import { SidePanelPageLayoutFieldsSettings } from '@/side-panel/pages/page-layout/components/SidePanelPageLayoutFieldsSettings';
|
||||
import { SidePanelPageLayoutIframeSettings } from '@/side-panel/pages/page-layout/components/SidePanelPageLayoutIframeSettings';
|
||||
import { SidePanelPageLayoutRecordTableSettings } from '@/side-panel/pages/page-layout/components/SidePanelPageLayoutRecordTableSettings';
|
||||
import { SidePanelDashboardChartSettings } from '@/side-panel/pages/page-layout/components/dashboard/SidePanelDashboardChartSettings';
|
||||
import { SidePanelDashboardIframeSettings } from '@/side-panel/pages/page-layout/components/dashboard/SidePanelDashboardIframeSettings';
|
||||
import { SidePanelDashboardRecordTableSettings } from '@/side-panel/pages/page-layout/components/dashboard/SidePanelDashboardRecordTableSettings';
|
||||
import { SidePanelRecordPageFieldSettings } from '@/side-panel/pages/page-layout/components/record-page/SidePanelRecordPageFieldSettings';
|
||||
import { SidePanelRecordPageFieldsSettings } from '@/side-panel/pages/page-layout/components/record-page/SidePanelRecordPageFieldsSettings';
|
||||
import { SidePanelPageLayoutDashboardWidgetTypeSelect } from '@/side-panel/pages/page-layout/components/SidePanelPageLayoutDashboardWidgetTypeSelect';
|
||||
import { SidePanelPageLayoutRecordPageWidgetTypeSelect } from '@/side-panel/pages/page-layout/components/SidePanelPageLayoutRecordPageWidgetTypeSelect';
|
||||
import { SidePanelPageLayoutTabSettings } from '@/side-panel/pages/page-layout/components/SidePanelPageLayoutTabSettings';
|
||||
import { SidePanelPageLayoutWidgetTypeSelect } from '@/side-panel/pages/page-layout/components/SidePanelPageLayoutWidgetTypeSelect';
|
||||
import { SidePanelMergeRecordPage } from '@/side-panel/pages/record-page/components/SidePanelMergeRecordPage';
|
||||
import { SidePanelRecordPage } from '@/side-panel/pages/record-page/components/SidePanelRecordPage';
|
||||
import { SidePanelUpdateMultipleRecords } from '@/side-panel/pages/record-page/components/SidePanelUpdateMultipleRecords';
|
||||
@@ -51,31 +52,35 @@ export const SIDE_PANEL_PAGES_CONFIG = new Map<SidePanelPages, React.ReactNode>(
|
||||
[SidePanelPages.AskAI, <SidePanelAskAIPage />],
|
||||
[SidePanelPages.ViewPreviousAIChats, <SidePanelAIChatThreadsPage />],
|
||||
[
|
||||
SidePanelPages.PageLayoutWidgetTypeSelect,
|
||||
<SidePanelPageLayoutWidgetTypeSelect />,
|
||||
SidePanelPages.PageLayoutDashboardWidgetTypeSelect,
|
||||
<SidePanelPageLayoutDashboardWidgetTypeSelect />,
|
||||
],
|
||||
[
|
||||
SidePanelPages.PageLayoutGraphTypeSelect,
|
||||
<SidePanelPageLayoutChartSettings />,
|
||||
SidePanelPages.DashboardChartSettings,
|
||||
<SidePanelDashboardChartSettings />,
|
||||
],
|
||||
[
|
||||
SidePanelPages.PageLayoutIframeSettings,
|
||||
<SidePanelPageLayoutIframeSettings />,
|
||||
SidePanelPages.DashboardIframeSettings,
|
||||
<SidePanelDashboardIframeSettings />,
|
||||
],
|
||||
[SidePanelPages.PageLayoutTabSettings, <SidePanelPageLayoutTabSettings />],
|
||||
[
|
||||
SidePanelPages.PageLayoutFieldsSettings,
|
||||
<SidePanelPageLayoutFieldsSettings />,
|
||||
SidePanelPages.DashboardRecordTableSettings,
|
||||
<SidePanelDashboardRecordTableSettings />,
|
||||
],
|
||||
[
|
||||
SidePanelPages.PageLayoutFieldSettings,
|
||||
<SidePanelPageLayoutFieldSettings />,
|
||||
SidePanelPages.RecordPageFieldsSettings,
|
||||
<SidePanelRecordPageFieldsSettings />,
|
||||
],
|
||||
[
|
||||
SidePanelPages.PageLayoutRecordTableSettings,
|
||||
<SidePanelPageLayoutRecordTableSettings />,
|
||||
SidePanelPages.RecordPageFieldSettings,
|
||||
<SidePanelRecordPageFieldSettings />,
|
||||
],
|
||||
[SidePanelPages.ViewFrontComponent, <SidePanelFrontComponentPage />],
|
||||
[
|
||||
SidePanelPages.PageLayoutRecordPageWidgetTypeSelect,
|
||||
<SidePanelPageLayoutRecordPageWidgetTypeSelect />,
|
||||
],
|
||||
[
|
||||
SidePanelPages.NavigationMenuItemEdit,
|
||||
<SidePanelNavigationMenuItemEditPage />,
|
||||
|
||||
+44
-23
@@ -1,24 +1,28 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
|
||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
|
||||
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { useIsDashboardPageLayout } from '@/side-panel/pages/page-layout/hooks/useIsDashboardPageLayout';
|
||||
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
|
||||
import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
import { WidgetType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useEditPageLayoutWidget = (pageLayoutIdFromProps?: string) => {
|
||||
export const useOpenWidgetSettingsInSidePanel = (
|
||||
pageLayoutIdFromProps?: string,
|
||||
) => {
|
||||
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
|
||||
PageLayoutComponentInstanceContext,
|
||||
pageLayoutIdFromProps,
|
||||
);
|
||||
|
||||
const isDashboardPageLayout = useIsDashboardPageLayout();
|
||||
|
||||
const setPageLayoutEditingWidgetId = useSetAtomComponentState(
|
||||
pageLayoutEditingWidgetIdComponentState,
|
||||
pageLayoutId,
|
||||
@@ -28,7 +32,7 @@ export const useEditPageLayoutWidget = (pageLayoutIdFromProps?: string) => {
|
||||
const { closeSidePanelMenu } = useSidePanelMenu();
|
||||
const setSidePanelPage = useSetAtomState(sidePanelPageState);
|
||||
|
||||
const handleEditWidget = useCallback(
|
||||
const openWidgetSettingsInSidePanel = useCallback(
|
||||
({
|
||||
widgetId,
|
||||
widgetType,
|
||||
@@ -37,8 +41,12 @@ export const useEditPageLayoutWidget = (pageLayoutIdFromProps?: string) => {
|
||||
widgetType: WidgetType;
|
||||
}) => {
|
||||
if (widgetType === WidgetType.IFRAME) {
|
||||
if (!isDashboardPageLayout) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigatePageLayoutSidePanel({
|
||||
sidePanelPage: SidePanelPages.PageLayoutIframeSettings,
|
||||
sidePanelPage: SidePanelPages.DashboardIframeSettings,
|
||||
pageTitle: t`Edit iFrame`,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
@@ -47,8 +55,12 @@ export const useEditPageLayoutWidget = (pageLayoutIdFromProps?: string) => {
|
||||
}
|
||||
|
||||
if (widgetType === WidgetType.GRAPH) {
|
||||
if (!isDashboardPageLayout) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigatePageLayoutSidePanel({
|
||||
sidePanelPage: SidePanelPages.PageLayoutGraphTypeSelect,
|
||||
sidePanelPage: SidePanelPages.DashboardChartSettings,
|
||||
pageTitle: t`Edit Graph`,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
@@ -57,28 +69,36 @@ export const useEditPageLayoutWidget = (pageLayoutIdFromProps?: string) => {
|
||||
}
|
||||
|
||||
if (widgetType === WidgetType.FIELDS) {
|
||||
navigatePageLayoutSidePanel({
|
||||
sidePanelPage: SidePanelPages.PageLayoutFieldsSettings,
|
||||
pageTitle: t`Edit Fields`,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
setPageLayoutEditingWidgetId(widgetId);
|
||||
if (!isDashboardPageLayout) {
|
||||
navigatePageLayoutSidePanel({
|
||||
sidePanelPage: SidePanelPages.RecordPageFieldsSettings,
|
||||
pageTitle: t`Edit Fields`,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
setPageLayoutEditingWidgetId(widgetId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (widgetType === WidgetType.FIELD) {
|
||||
navigatePageLayoutSidePanel({
|
||||
sidePanelPage: SidePanelPages.PageLayoutFieldSettings,
|
||||
pageTitle: t`Field widget`,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
setPageLayoutEditingWidgetId(widgetId);
|
||||
if (!isDashboardPageLayout) {
|
||||
navigatePageLayoutSidePanel({
|
||||
sidePanelPage: SidePanelPages.RecordPageFieldSettings,
|
||||
pageTitle: t`Field widget`,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
setPageLayoutEditingWidgetId(widgetId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (widgetType === WidgetType.RECORD_TABLE) {
|
||||
if (!isDashboardPageLayout) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigatePageLayoutSidePanel({
|
||||
sidePanelPage: SidePanelPages.PageLayoutRecordTableSettings,
|
||||
sidePanelPage: SidePanelPages.DashboardRecordTableSettings,
|
||||
pageTitle: t`Edit Record Table`,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
@@ -96,6 +116,7 @@ export const useEditPageLayoutWidget = (pageLayoutIdFromProps?: string) => {
|
||||
closeSidePanelMenu();
|
||||
},
|
||||
[
|
||||
isDashboardPageLayout,
|
||||
setPageLayoutEditingWidgetId,
|
||||
navigatePageLayoutSidePanel,
|
||||
closeSidePanelMenu,
|
||||
@@ -104,6 +125,6 @@ export const useEditPageLayoutWidget = (pageLayoutIdFromProps?: string) => {
|
||||
);
|
||||
|
||||
return {
|
||||
handleEditWidget,
|
||||
openWidgetSettingsInSidePanel,
|
||||
};
|
||||
};
|
||||
+9
-7
@@ -8,10 +8,12 @@ import { recordStoreFamilyState } from '@/object-record/record-store/states/reco
|
||||
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
|
||||
import { widgetInsertionContextComponentState } from '@/page-layout/states/widgetInsertionContextComponentState';
|
||||
import { SIDE_PANEL_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelComponentInstanceId';
|
||||
import { SIDE_PANEL_CONTEXT_CHIP_GROUPS_DROPDOWN_ID } from '@/side-panel/constants/SidePanelContextChipGroupsDropdownId';
|
||||
import { SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelPreviousComponentInstanceId';
|
||||
import { SIDE_PANEL_SELECTABLE_LIST_ID } from '@/side-panel/constants/SidePanelSelectableListId';
|
||||
import { isPageLayoutSidePanelPage } from '@/side-panel/pages/page-layout/utils/isPageLayoutSidePanelPage';
|
||||
import { hasUserSelectedSidePanelListItemState } from '@/side-panel/states/hasUserSelectedSidePanelListItemState';
|
||||
import { isSidePanelClosingState } from '@/side-panel/states/isSidePanelClosingState';
|
||||
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
|
||||
@@ -69,13 +71,7 @@ export const useSidePanelCloseAnimationCompleteCleanup = () => {
|
||||
resetContextStoreStates(SIDE_PANEL_COMPONENT_INSTANCE_ID);
|
||||
resetContextStoreStates(SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID);
|
||||
|
||||
const isPageLayoutEditingPage =
|
||||
currentPage === SidePanelPages.PageLayoutWidgetTypeSelect ||
|
||||
currentPage === SidePanelPages.PageLayoutGraphTypeSelect ||
|
||||
currentPage === SidePanelPages.PageLayoutIframeSettings ||
|
||||
currentPage === SidePanelPages.PageLayoutTabSettings;
|
||||
|
||||
if (isPageLayoutEditingPage) {
|
||||
if (isDefined(currentPage) && isPageLayoutSidePanelPage(currentPage)) {
|
||||
if (
|
||||
targetedRecordsRule.mode === 'selection' &&
|
||||
targetedRecordsRule.selectedRecordIds.length === 1
|
||||
@@ -102,6 +98,12 @@ export const useSidePanelCloseAnimationCompleteCleanup = () => {
|
||||
}),
|
||||
null,
|
||||
);
|
||||
store.set(
|
||||
widgetInsertionContextComponentState.atomFamily({
|
||||
instanceId: record.pageLayoutId,
|
||||
}),
|
||||
null,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -38,7 +38,7 @@ import {
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const SidePanelPageLayoutWidgetTypeSelect = () => {
|
||||
export const SidePanelPageLayoutDashboardWidgetTypeSelect = () => {
|
||||
const { pageLayoutId, recordId } = usePageLayoutIdFromContextStore();
|
||||
|
||||
const { closeSidePanelMenu } = useSidePanelMenu();
|
||||
@@ -132,7 +132,7 @@ export const SidePanelPageLayoutWidgetTypeSelect = () => {
|
||||
}
|
||||
|
||||
navigatePageLayoutSidePanel({
|
||||
sidePanelPage: SidePanelPages.PageLayoutGraphTypeSelect,
|
||||
sidePanelPage: SidePanelPages.DashboardChartSettings,
|
||||
focusTitleInput: true,
|
||||
});
|
||||
};
|
||||
@@ -153,7 +153,7 @@ export const SidePanelPageLayoutWidgetTypeSelect = () => {
|
||||
}
|
||||
|
||||
navigatePageLayoutSidePanel({
|
||||
sidePanelPage: SidePanelPages.PageLayoutIframeSettings,
|
||||
sidePanelPage: SidePanelPages.DashboardIframeSettings,
|
||||
focusTitleInput: true,
|
||||
});
|
||||
};
|
||||
@@ -196,7 +196,7 @@ export const SidePanelPageLayoutWidgetTypeSelect = () => {
|
||||
}
|
||||
|
||||
navigatePageLayoutSidePanel({
|
||||
sidePanelPage: SidePanelPages.PageLayoutRecordTableSettings,
|
||||
sidePanelPage: SidePanelPages.DashboardRecordTableSettings,
|
||||
focusTitleInput: false,
|
||||
});
|
||||
};
|
||||
+440
@@ -0,0 +1,440 @@
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { FIND_MANY_FRONT_COMPONENTS } from '@/front-components/graphql/queries/findManyFrontComponents';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useFieldListFieldMetadataItems } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataItems';
|
||||
import { useInsertCreatedWidgetAtContext } from '@/page-layout/hooks/useInsertCreatedWidgetAtContext';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { widgetInsertionContextComponentState } from '@/page-layout/states/widgetInsertionContextComponentState';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab';
|
||||
import { createDefaultFieldWidget } from '@/page-layout/utils/createDefaultFieldWidget';
|
||||
import { createDefaultFieldsWidget } from '@/page-layout/utils/createDefaultFieldsWidget';
|
||||
import { getTabListInstanceIdFromPageLayoutAndRecord } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutAndRecord';
|
||||
import { getWidgetConfigurationViewId } from '@/page-layout/utils/getWidgetConfigurationViewId';
|
||||
import { isVerticalListPosition } from '@/page-layout/utils/isVerticalListPosition';
|
||||
import { removeWidgetFromTab } from '@/page-layout/utils/removeWidgetFromTab';
|
||||
import { useCreateViewForFieldsWidget } from '@/page-layout/widgets/fields/hooks/useCreateViewForFieldsWidget';
|
||||
import { useDeleteViewForFieldsWidget } from '@/page-layout/widgets/fields/hooks/useDeleteViewForFieldsWidget';
|
||||
import { useDeleteViewForRecordTableWidget } from '@/page-layout/widgets/record-table/hooks/useDeleteViewForRecordTableWidget';
|
||||
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
|
||||
import { SidePanelList } from '@/side-panel/components/SidePanelList';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
|
||||
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
|
||||
import { getFrontComponentWidgetTypeSelectItemId } from '@/side-panel/pages/page-layout/utils/getFrontComponentWidgetTypeSelectItemId';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconApps, IconList } from 'twenty-ui/display';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import {
|
||||
type FrontComponent,
|
||||
PageLayoutTabLayoutMode,
|
||||
WidgetConfigurationType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const SidePanelPageLayoutRecordPageWidgetTypeSelect = () => {
|
||||
const {
|
||||
pageLayoutId,
|
||||
recordId,
|
||||
objectNameSingular: targetObjectNameSingular,
|
||||
} = usePageLayoutIdFromContextStore();
|
||||
|
||||
const { closeSidePanelMenu } = useSidePanelMenu();
|
||||
|
||||
const { navigatePageLayoutSidePanel } = useNavigatePageLayoutSidePanel();
|
||||
|
||||
const pageLayoutDraft = useAtomComponentStateValue(
|
||||
pageLayoutDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const pageLayoutDraftState = useAtomComponentStateCallbackState(
|
||||
pageLayoutDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const [pageLayoutEditingWidgetId, setPageLayoutEditingWidgetId] =
|
||||
useAtomComponentState(
|
||||
pageLayoutEditingWidgetIdComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const widgetInsertionContext = useAtomComponentStateValue(
|
||||
widgetInsertionContextComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const tabListInstanceId = getTabListInstanceIdFromPageLayoutAndRecord({
|
||||
pageLayoutId,
|
||||
layoutType: pageLayoutDraft.type,
|
||||
targetRecordIdentifier: { id: recordId, targetObjectNameSingular: '' },
|
||||
});
|
||||
|
||||
const activeTabId = useAtomComponentStateValue(
|
||||
activeTabIdComponentState,
|
||||
tabListInstanceId,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const { insertCreatedWidgetAtContext } =
|
||||
useInsertCreatedWidgetAtContext(pageLayoutId);
|
||||
|
||||
const { createViewForFieldsWidget } = useCreateViewForFieldsWidget();
|
||||
|
||||
const { deleteViewForFieldsWidget } = useDeleteViewForFieldsWidget();
|
||||
|
||||
const { deleteViewForRecordTableWidget } =
|
||||
useDeleteViewForRecordTableWidget();
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular: targetObjectNameSingular,
|
||||
});
|
||||
|
||||
const { boxedRelationFieldMetadataItems } = useFieldListFieldMetadataItems({
|
||||
objectNameSingular: targetObjectNameSingular,
|
||||
});
|
||||
|
||||
const editingWidgetTab = isDefined(pageLayoutEditingWidgetId)
|
||||
? pageLayoutDraft.tabs.find((tab) =>
|
||||
tab.widgets.some((widget) => widget.id === pageLayoutEditingWidgetId),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const tabId = editingWidgetTab?.id ?? activeTabId;
|
||||
|
||||
const isReplaceMode =
|
||||
isDefined(pageLayoutEditingWidgetId) && !isDefined(widgetInsertionContext);
|
||||
|
||||
const existingWidget = isReplaceMode
|
||||
? pageLayoutDraft.tabs
|
||||
.flatMap((tab) => tab.widgets)
|
||||
.find((widget) => widget.id === pageLayoutEditingWidgetId)
|
||||
: undefined;
|
||||
|
||||
const getExistingWidgetPositionIndex = useCallback(() => {
|
||||
if (!isDefined(existingWidget?.position)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isVerticalListPosition(existingWidget.position)) {
|
||||
return existingWidget.position.index;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [existingWidget]);
|
||||
|
||||
const removeExistingWidgetIfReplacing = useCallback(() => {
|
||||
if (
|
||||
!isReplaceMode ||
|
||||
!isDefined(pageLayoutEditingWidgetId) ||
|
||||
!isDefined(tabId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDefined(existingWidget)) {
|
||||
const viewId = getWidgetConfigurationViewId(existingWidget.configuration);
|
||||
|
||||
if (isDefined(viewId)) {
|
||||
if (existingWidget.type === WidgetType.RECORD_TABLE) {
|
||||
deleteViewForRecordTableWidget(viewId);
|
||||
}
|
||||
|
||||
if (existingWidget.type === WidgetType.FIELDS) {
|
||||
deleteViewForFieldsWidget(viewId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
store.set(pageLayoutDraftState, (prev) => ({
|
||||
...prev,
|
||||
tabs: removeWidgetFromTab(prev.tabs, tabId, pageLayoutEditingWidgetId),
|
||||
}));
|
||||
}, [
|
||||
deleteViewForFieldsWidget,
|
||||
deleteViewForRecordTableWidget,
|
||||
existingWidget,
|
||||
isReplaceMode,
|
||||
pageLayoutDraftState,
|
||||
pageLayoutEditingWidgetId,
|
||||
store,
|
||||
tabId,
|
||||
]);
|
||||
|
||||
const { data: frontComponentsData } = useQuery<{
|
||||
frontComponents: FrontComponent[];
|
||||
}>(FIND_MANY_FRONT_COMPONENTS);
|
||||
|
||||
const frontComponents = frontComponentsData?.frontComponents ?? [];
|
||||
|
||||
const frontComponentsWithSelectItemId = frontComponents.map(
|
||||
(frontComponent) => ({
|
||||
frontComponent,
|
||||
selectItemId: getFrontComponentWidgetTypeSelectItemId(frontComponent.id),
|
||||
}),
|
||||
);
|
||||
|
||||
const handleCreateFieldsWidget = useCallback(async () => {
|
||||
if (!isDefined(tabId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const replacePositionIndex = getExistingWidgetPositionIndex();
|
||||
|
||||
const viewId = await createViewForFieldsWidget({
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
viewName: `${objectMetadataItem.labelSingular} Fields`,
|
||||
});
|
||||
|
||||
if (viewId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
removeExistingWidgetIfReplacing();
|
||||
|
||||
const updatedPageLayout = store.get(pageLayoutDraftState);
|
||||
const activeTab = updatedPageLayout.tabs.find((tab) => tab.id === tabId);
|
||||
const positionIndex =
|
||||
replacePositionIndex ?? activeTab?.widgets.length ?? 0;
|
||||
const widgetId = uuidv4();
|
||||
|
||||
const newWidget = createDefaultFieldsWidget({
|
||||
id: widgetId,
|
||||
pageLayoutTabId: tabId,
|
||||
viewId,
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
positionIndex,
|
||||
});
|
||||
|
||||
store.set(pageLayoutDraftState, (prev) => ({
|
||||
...prev,
|
||||
tabs: addWidgetToTab(prev.tabs, tabId, newWidget),
|
||||
}));
|
||||
|
||||
setPageLayoutEditingWidgetId(widgetId);
|
||||
insertCreatedWidgetAtContext(widgetId);
|
||||
|
||||
navigatePageLayoutSidePanel({
|
||||
sidePanelPage: SidePanelPages.RecordPageFieldsSettings,
|
||||
focusTitleInput: true,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
}, [
|
||||
createViewForFieldsWidget,
|
||||
getExistingWidgetPositionIndex,
|
||||
insertCreatedWidgetAtContext,
|
||||
navigatePageLayoutSidePanel,
|
||||
objectMetadataItem.id,
|
||||
objectMetadataItem.labelSingular,
|
||||
pageLayoutDraftState,
|
||||
removeExistingWidgetIfReplacing,
|
||||
setPageLayoutEditingWidgetId,
|
||||
store,
|
||||
tabId,
|
||||
]);
|
||||
|
||||
const handleCreateFieldWidget = useCallback(() => {
|
||||
if (!isDefined(tabId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const replacePositionIndex = getExistingWidgetPositionIndex();
|
||||
removeExistingWidgetIfReplacing();
|
||||
|
||||
const updatedPageLayout = store.get(pageLayoutDraftState);
|
||||
const activeTab = updatedPageLayout.tabs.find((tab) => tab.id === tabId);
|
||||
const existingWidgets = activeTab?.widgets ?? [];
|
||||
const positionIndex = replacePositionIndex ?? existingWidgets.length;
|
||||
const widgetId = uuidv4();
|
||||
|
||||
const usedFieldMetadataIds = new Set(
|
||||
existingWidgets
|
||||
.filter(
|
||||
(widget) =>
|
||||
widget.configuration.configurationType ===
|
||||
WidgetConfigurationType.FIELD,
|
||||
)
|
||||
.map((widget) => {
|
||||
const configuration = widget.configuration as {
|
||||
fieldMetadataId: string;
|
||||
};
|
||||
return configuration.fieldMetadataId;
|
||||
}),
|
||||
);
|
||||
|
||||
const unusedRelationField = boxedRelationFieldMetadataItems.find(
|
||||
(field) => !usedFieldMetadataIds.has(field.id),
|
||||
);
|
||||
|
||||
const selectedField =
|
||||
unusedRelationField ?? boxedRelationFieldMetadataItems[0];
|
||||
|
||||
const fieldMetadataId = selectedField?.id ?? '';
|
||||
const title = selectedField?.label ?? '';
|
||||
|
||||
const newWidget = createDefaultFieldWidget({
|
||||
id: widgetId,
|
||||
pageLayoutTabId: tabId,
|
||||
title,
|
||||
fieldMetadataId,
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
positionIndex,
|
||||
});
|
||||
|
||||
store.set(pageLayoutDraftState, (prev) => ({
|
||||
...prev,
|
||||
tabs: addWidgetToTab(prev.tabs, tabId, newWidget),
|
||||
}));
|
||||
|
||||
setPageLayoutEditingWidgetId(widgetId);
|
||||
insertCreatedWidgetAtContext(widgetId);
|
||||
|
||||
navigatePageLayoutSidePanel({
|
||||
sidePanelPage: SidePanelPages.RecordPageFieldSettings,
|
||||
focusTitleInput: true,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
}, [
|
||||
boxedRelationFieldMetadataItems,
|
||||
getExistingWidgetPositionIndex,
|
||||
insertCreatedWidgetAtContext,
|
||||
navigatePageLayoutSidePanel,
|
||||
objectMetadataItem.id,
|
||||
pageLayoutDraftState,
|
||||
removeExistingWidgetIfReplacing,
|
||||
setPageLayoutEditingWidgetId,
|
||||
store,
|
||||
tabId,
|
||||
]);
|
||||
|
||||
const handleCreateFrontComponentWidget = useCallback(
|
||||
(frontComponent: FrontComponent) => {
|
||||
if (!isDefined(tabId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const replacePositionIndex = getExistingWidgetPositionIndex();
|
||||
removeExistingWidgetIfReplacing();
|
||||
|
||||
const updatedPageLayout = store.get(pageLayoutDraftState);
|
||||
const activeTab = updatedPageLayout.tabs.find((tab) => tab.id === tabId);
|
||||
const positionIndex =
|
||||
replacePositionIndex ?? activeTab?.widgets.length ?? 0;
|
||||
const widgetId = uuidv4();
|
||||
|
||||
const newWidget: PageLayoutWidget = {
|
||||
__typename: 'PageLayoutWidget',
|
||||
id: widgetId,
|
||||
pageLayoutTabId: tabId,
|
||||
title: frontComponent.name,
|
||||
type: WidgetType.FRONT_COMPONENT,
|
||||
configuration: {
|
||||
__typename: 'FrontComponentConfiguration',
|
||||
configurationType: WidgetConfigurationType.FRONT_COMPONENT,
|
||||
frontComponentId: frontComponent.id,
|
||||
},
|
||||
gridPosition: {
|
||||
__typename: 'GridPosition',
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 1,
|
||||
columnSpan: 12,
|
||||
},
|
||||
position: {
|
||||
__typename: 'PageLayoutWidgetVerticalListPosition',
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
index: positionIndex,
|
||||
},
|
||||
objectMetadataId: null,
|
||||
isOverridden: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
};
|
||||
|
||||
store.set(pageLayoutDraftState, (prev) => ({
|
||||
...prev,
|
||||
tabs: addWidgetToTab(prev.tabs, tabId, newWidget),
|
||||
}));
|
||||
|
||||
setPageLayoutEditingWidgetId(widgetId);
|
||||
insertCreatedWidgetAtContext(widgetId);
|
||||
|
||||
closeSidePanelMenu();
|
||||
},
|
||||
[
|
||||
closeSidePanelMenu,
|
||||
getExistingWidgetPositionIndex,
|
||||
insertCreatedWidgetAtContext,
|
||||
pageLayoutDraftState,
|
||||
removeExistingWidgetIfReplacing,
|
||||
setPageLayoutEditingWidgetId,
|
||||
store,
|
||||
tabId,
|
||||
],
|
||||
);
|
||||
|
||||
const selectableItemIds = [
|
||||
'fields',
|
||||
'field',
|
||||
...frontComponentsWithSelectItemId.map(({ selectItemId }) => selectItemId),
|
||||
];
|
||||
|
||||
return (
|
||||
<SidePanelList commandGroups={[]} selectableItemIds={selectableItemIds}>
|
||||
<SidePanelGroup heading={t`Widget type`}>
|
||||
<SelectableListItem itemId="fields" onEnter={handleCreateFieldsWidget}>
|
||||
<CommandMenuItem
|
||||
Icon={IconList}
|
||||
label={t`Fields`}
|
||||
id="fields"
|
||||
onClick={handleCreateFieldsWidget}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
<SelectableListItem itemId="field" onEnter={handleCreateFieldWidget}>
|
||||
<CommandMenuItem
|
||||
Icon={IconList}
|
||||
label={t`Field`}
|
||||
id="field"
|
||||
onClick={handleCreateFieldWidget}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
</SidePanelGroup>
|
||||
|
||||
{frontComponentsWithSelectItemId.length > 0 && (
|
||||
<SidePanelGroup heading={t`Front Components`}>
|
||||
{frontComponentsWithSelectItemId.map(
|
||||
({ frontComponent, selectItemId }) => (
|
||||
<SelectableListItem
|
||||
key={frontComponent.id}
|
||||
itemId={selectItemId}
|
||||
onEnter={() => handleCreateFrontComponentWidget(frontComponent)}
|
||||
>
|
||||
<CommandMenuItem
|
||||
Icon={IconApps}
|
||||
label={frontComponent.name}
|
||||
id={selectItemId}
|
||||
onClick={() =>
|
||||
handleCreateFrontComponentWidget(frontComponent)
|
||||
}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
),
|
||||
)}
|
||||
</SidePanelGroup>
|
||||
)}
|
||||
</SidePanelList>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -3,9 +3,9 @@ import { useDuplicatePageLayoutWidget } from '@/page-layout/hooks/useDuplicatePa
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { OptionsDropdownMenu } from '@/ui/layout/dropdown/components/OptionsDropdownMenu';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { SidePanelFooter } from '@/ui/layout/side-panel/components/SidePanelFooter';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { SidePanelFooter } from '@/ui/layout/side-panel/components/SidePanelFooter';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useId } from 'react';
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { useDeletePageLayoutWidget } from '@/page-layout/hooks/useDeletePageLayoutWidget';
|
||||
import { useResetPageLayoutWidgetToDefault } from '@/page-layout/hooks/useResetPageLayoutWidgetToDefault';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
|
||||
import { WIDGET_SETTINGS_SELECTABLE_ITEM_IDS } from '@/side-panel/pages/page-layout/constants/settings/WidgetSettingsSelectableItemIds';
|
||||
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconRefreshDot,
|
||||
IconSwitchHorizontal,
|
||||
IconTrash,
|
||||
} from 'twenty-ui/display';
|
||||
|
||||
const RESET_WIDGET_TO_DEFAULT_MODAL_ID = 'reset-widget-to-default-modal';
|
||||
|
||||
type WidgetSettingsManageSectionProps = {
|
||||
pageLayoutId: string;
|
||||
};
|
||||
|
||||
export const WidgetSettingsManageSection = ({
|
||||
pageLayoutId,
|
||||
}: WidgetSettingsManageSectionProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const pageLayoutEditingWidgetId = useAtomComponentStateValue(
|
||||
pageLayoutEditingWidgetIdComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const { deletePageLayoutWidget } = useDeletePageLayoutWidget(pageLayoutId);
|
||||
|
||||
const { resetPageLayoutWidgetToDefault } =
|
||||
useResetPageLayoutWidgetToDefault(pageLayoutId);
|
||||
|
||||
const { navigatePageLayoutSidePanel } = useNavigatePageLayoutSidePanel();
|
||||
|
||||
const { openModal } = useModal();
|
||||
|
||||
if (!isDefined(pageLayoutEditingWidgetId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleResetToDefault = () => {
|
||||
openModal(RESET_WIDGET_TO_DEFAULT_MODAL_ID);
|
||||
};
|
||||
|
||||
const handleConfirmReset = () => {
|
||||
resetPageLayoutWidgetToDefault(pageLayoutEditingWidgetId);
|
||||
};
|
||||
|
||||
const handleReplaceWidget = () => {
|
||||
navigatePageLayoutSidePanel({
|
||||
sidePanelPage: SidePanelPages.PageLayoutRecordPageWidgetTypeSelect,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteWidget = () => {
|
||||
deletePageLayoutWidget(pageLayoutEditingWidgetId);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidePanelGroup heading={t`Manage`}>
|
||||
<SelectableListItem
|
||||
itemId={WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.RESET_TO_DEFAULT}
|
||||
onEnter={handleResetToDefault}
|
||||
>
|
||||
<CommandMenuItem
|
||||
id={WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.RESET_TO_DEFAULT}
|
||||
Icon={IconRefreshDot}
|
||||
label={t`Reset to default`}
|
||||
onClick={handleResetToDefault}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
<SelectableListItem
|
||||
itemId={WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.REPLACE_WIDGET}
|
||||
onEnter={handleReplaceWidget}
|
||||
>
|
||||
<CommandMenuItem
|
||||
id={WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.REPLACE_WIDGET}
|
||||
Icon={IconSwitchHorizontal}
|
||||
label={t`Replace widget`}
|
||||
hasSubMenu
|
||||
onClick={handleReplaceWidget}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
<SelectableListItem
|
||||
itemId={WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.DELETE_WIDGET}
|
||||
onEnter={handleDeleteWidget}
|
||||
>
|
||||
<CommandMenuItem
|
||||
id={WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.DELETE_WIDGET}
|
||||
Icon={IconTrash}
|
||||
label={t`Delete widget`}
|
||||
onClick={handleDeleteWidget}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
</SidePanelGroup>
|
||||
<ConfirmationModal
|
||||
modalInstanceId={RESET_WIDGET_TO_DEFAULT_MODAL_ID}
|
||||
title={t`Reset to default`}
|
||||
subtitle={t`This will cancel all modifications done on the widget. This action cannot be undone.`}
|
||||
onConfirmClick={handleConfirmReset}
|
||||
confirmButtonText={t`Reset`}
|
||||
confirmButtonAccent="danger"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { CommandMenuItemDropdown } from '@/command-menu/components/CommandMenuItemDropdown';
|
||||
import { useCanMovePageLayoutWidgetDown } from '@/page-layout/hooks/useCanMovePageLayoutWidgetDown';
|
||||
import { useCanMovePageLayoutWidgetUp } from '@/page-layout/hooks/useCanMovePageLayoutWidgetUp';
|
||||
import { useMovePageLayoutWidgetDown } from '@/page-layout/hooks/useMovePageLayoutWidgetDown';
|
||||
import { useMovePageLayoutWidgetUp } from '@/page-layout/hooks/useMovePageLayoutWidgetUp';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { widgetInsertionContextComponentState } from '@/page-layout/states/widgetInsertionContextComponentState';
|
||||
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
|
||||
import { MoveToTabDropdownContent } from '@/side-panel/pages/page-layout/components/dropdown-content/MoveToTabDropdownContent';
|
||||
import { WIDGET_SETTINGS_SELECTABLE_ITEM_IDS } from '@/side-panel/pages/page-layout/constants/settings/WidgetSettingsSelectableItemIds';
|
||||
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useStore } from 'jotai';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconArrowsVertical,
|
||||
IconChevronDown,
|
||||
IconChevronUp,
|
||||
IconRowInsertBottom,
|
||||
IconRowInsertTop,
|
||||
} from 'twenty-ui/display';
|
||||
import { PageLayoutTabLayoutMode } from '~/generated-metadata/graphql';
|
||||
|
||||
type WidgetSettingsPlacementSectionProps = {
|
||||
pageLayoutId: string;
|
||||
};
|
||||
|
||||
export const WidgetSettingsPlacementSection = ({
|
||||
pageLayoutId,
|
||||
}: WidgetSettingsPlacementSectionProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const pageLayoutEditingWidgetId = useAtomComponentStateValue(
|
||||
pageLayoutEditingWidgetIdComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const pageLayoutDraft = useAtomComponentStateValue(
|
||||
pageLayoutDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const widgetInsertionContextState = useAtomComponentStateCallbackState(
|
||||
widgetInsertionContextComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const { movePageLayoutWidgetUp } = useMovePageLayoutWidgetUp(pageLayoutId);
|
||||
const { movePageLayoutWidgetDown } =
|
||||
useMovePageLayoutWidgetDown(pageLayoutId);
|
||||
const { canMovePageLayoutWidgetUp } =
|
||||
useCanMovePageLayoutWidgetUp(pageLayoutId);
|
||||
const { canMovePageLayoutWidgetDown } =
|
||||
useCanMovePageLayoutWidgetDown(pageLayoutId);
|
||||
|
||||
const { navigatePageLayoutSidePanel } = useNavigatePageLayoutSidePanel();
|
||||
|
||||
if (!isDefined(pageLayoutEditingWidgetId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentTab = pageLayoutDraft.tabs.find((tab) =>
|
||||
tab.widgets.some((widget) => widget.id === pageLayoutEditingWidgetId),
|
||||
);
|
||||
|
||||
if (
|
||||
!isDefined(currentTab) ||
|
||||
currentTab.layoutMode !== PageLayoutTabLayoutMode.VERTICAL_LIST
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const showMoveUp = canMovePageLayoutWidgetUp(pageLayoutEditingWidgetId);
|
||||
const showMoveDown = canMovePageLayoutWidgetDown(pageLayoutEditingWidgetId);
|
||||
|
||||
const handleMoveUp = () => {
|
||||
movePageLayoutWidgetUp(pageLayoutEditingWidgetId);
|
||||
};
|
||||
|
||||
const handleMoveDown = () => {
|
||||
movePageLayoutWidgetDown(pageLayoutEditingWidgetId);
|
||||
};
|
||||
|
||||
const handleAddWidgetAbove = () => {
|
||||
store.set(widgetInsertionContextState, {
|
||||
targetWidgetId: pageLayoutEditingWidgetId,
|
||||
direction: 'above',
|
||||
});
|
||||
|
||||
navigatePageLayoutSidePanel({
|
||||
sidePanelPage: SidePanelPages.PageLayoutRecordPageWidgetTypeSelect,
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddWidgetBelow = () => {
|
||||
store.set(widgetInsertionContextState, {
|
||||
targetWidgetId: pageLayoutEditingWidgetId,
|
||||
direction: 'below',
|
||||
});
|
||||
|
||||
navigatePageLayoutSidePanel({
|
||||
sidePanelPage: SidePanelPages.PageLayoutRecordPageWidgetTypeSelect,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<SidePanelGroup heading={t`Placement`}>
|
||||
{showMoveUp && (
|
||||
<SelectableListItem
|
||||
itemId={WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_UP}
|
||||
onEnter={handleMoveUp}
|
||||
>
|
||||
<CommandMenuItem
|
||||
id={WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_UP}
|
||||
Icon={IconChevronUp}
|
||||
label={t`Move Up`}
|
||||
onClick={handleMoveUp}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
)}
|
||||
{showMoveDown && (
|
||||
<SelectableListItem
|
||||
itemId={WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_DOWN}
|
||||
onEnter={handleMoveDown}
|
||||
>
|
||||
<CommandMenuItem
|
||||
id={WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_DOWN}
|
||||
Icon={IconChevronDown}
|
||||
label={t`Move Down`}
|
||||
onClick={handleMoveDown}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
)}
|
||||
<SelectableListItem
|
||||
itemId={WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_TO_TAB}
|
||||
>
|
||||
<CommandMenuItemDropdown
|
||||
id={WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_TO_TAB}
|
||||
label={t`Move to another tab`}
|
||||
Icon={IconArrowsVertical}
|
||||
dropdownId={WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_TO_TAB}
|
||||
dropdownComponents={
|
||||
<DropdownContent>
|
||||
<MoveToTabDropdownContent />
|
||||
</DropdownContent>
|
||||
}
|
||||
dropdownPlacement="bottom-end"
|
||||
/>
|
||||
</SelectableListItem>
|
||||
<SelectableListItem
|
||||
itemId={WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.ADD_WIDGET_ABOVE}
|
||||
onEnter={handleAddWidgetAbove}
|
||||
>
|
||||
<CommandMenuItem
|
||||
id={WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.ADD_WIDGET_ABOVE}
|
||||
Icon={IconRowInsertTop}
|
||||
label={t`Add widget above`}
|
||||
onClick={handleAddWidgetAbove}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
<SelectableListItem
|
||||
itemId={WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.ADD_WIDGET_BELOW}
|
||||
onEnter={handleAddWidgetBelow}
|
||||
>
|
||||
<CommandMenuItem
|
||||
id={WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.ADD_WIDGET_BELOW}
|
||||
Icon={IconRowInsertBottom}
|
||||
label={t`Add widget below`}
|
||||
onClick={handleAddWidgetBelow}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
</SidePanelGroup>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -15,7 +15,7 @@ const StyledContainer = styled.div`
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
export const SidePanelPageLayoutChartSettings = () => {
|
||||
export const SidePanelDashboardChartSettings = () => {
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStore();
|
||||
|
||||
const pageLayoutDraft = useAtomComponentStateValue(
|
||||
+23
-17
@@ -1,5 +1,8 @@
|
||||
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
|
||||
import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget';
|
||||
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
|
||||
import { SidePanelGroupFormContainer } from '@/side-panel/components/SidePanelGroupFormContainer';
|
||||
import { SidePanelList } from '@/side-panel/components/SidePanelList';
|
||||
import { WidgetSettingsFooter } from '@/side-panel/pages/page-layout/components/WidgetSettingsFooter';
|
||||
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
|
||||
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
@@ -8,24 +11,22 @@ import { t } from '@lingui/core/macro';
|
||||
import { isNonEmptyString, isString } from '@sniptt/guards';
|
||||
import { useState } from 'react';
|
||||
import { isDefined, isValidUrl } from 'twenty-shared/utils';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { WidgetConfigurationType } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledOuterContainer = styled.div`
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
const StyledSidePanelContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[3]};
|
||||
padding: ${themeCssVariables.spacing[2]};
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export const SidePanelPageLayoutIframeSettings = () => {
|
||||
export const SidePanelDashboardIframeSettings = () => {
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStore();
|
||||
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
@@ -84,17 +85,22 @@ export const SidePanelPageLayoutIframeSettings = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledOuterContainer>
|
||||
<StyledContainer>
|
||||
<FormTextFieldInput
|
||||
label={t`URL to Embed`}
|
||||
placeholder={t`https://example.com/embed`}
|
||||
defaultValue={url}
|
||||
onChange={handleUrlChange}
|
||||
error={urlError}
|
||||
/>
|
||||
</StyledContainer>
|
||||
<StyledContainer>
|
||||
<StyledSidePanelContainer>
|
||||
<SidePanelList commandGroups={[]} selectableItemIds={[]}>
|
||||
<SidePanelGroup heading={t`URL to Embed`}>
|
||||
<SidePanelGroupFormContainer>
|
||||
<FormTextFieldInput
|
||||
placeholder={t`https://example.com/embed`}
|
||||
defaultValue={url}
|
||||
onChange={handleUrlChange}
|
||||
error={urlError}
|
||||
/>
|
||||
</SidePanelGroupFormContainer>
|
||||
</SidePanelGroup>
|
||||
</SidePanelList>
|
||||
</StyledSidePanelContainer>
|
||||
<WidgetSettingsFooter pageLayoutId={pageLayoutId} />
|
||||
</StyledOuterContainer>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -36,7 +36,7 @@ const StyledSettingsContainer = styled.div`
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export const SidePanelPageLayoutRecordTableSettings = () => {
|
||||
export const SidePanelDashboardRecordTableSettings = () => {
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStore();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
const { navigateToSidePanelSubPage } = useSidePanelSubPageHistory();
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { useMoveWidgetToTab } from '@/page-layout/hooks/useMoveWidgetToTab';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
import { PageLayoutTabLayoutMode } from '~/generated-metadata/graphql';
|
||||
|
||||
export const MoveToTabDropdownContent = () => {
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStore();
|
||||
|
||||
const pageLayoutDraft = useAtomComponentStateValue(
|
||||
pageLayoutDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const pageLayoutEditingWidgetId = useAtomComponentStateValue(
|
||||
pageLayoutEditingWidgetIdComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const { moveWidgetToTab } = useMoveWidgetToTab(pageLayoutId);
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const currentTab = isDefined(pageLayoutEditingWidgetId)
|
||||
? pageLayoutDraft.tabs.find((tab) =>
|
||||
tab.widgets.some((widget) => widget.id === pageLayoutEditingWidgetId),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const eligibleTabs = pageLayoutDraft.tabs.filter(
|
||||
(tab) =>
|
||||
tab.layoutMode === PageLayoutTabLayoutMode.VERTICAL_LIST &&
|
||||
tab.id !== currentTab?.id,
|
||||
);
|
||||
|
||||
if (!isDefined(pageLayoutEditingWidgetId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (eligibleTabs.length === 0) {
|
||||
return (
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem text={t`No available tabs`} />
|
||||
</DropdownMenuItemsContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenuItemsContainer>
|
||||
{eligibleTabs.map((tab) => (
|
||||
<MenuItem
|
||||
key={tab.id}
|
||||
text={tab.title ?? ''}
|
||||
onClick={() => {
|
||||
moveWidgetToTab(pageLayoutEditingWidgetId, tab.id);
|
||||
closeDropdown();
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
);
|
||||
};
|
||||
+18
-4
@@ -4,9 +4,12 @@ import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
|
||||
import { SidePanelList } from '@/side-panel/components/SidePanelList';
|
||||
import { FieldWidgetFieldDropdownContent } from '@/side-panel/pages/page-layout/components/dropdown-content/FieldWidgetFieldDropdownContent';
|
||||
import { FieldWidgetLayoutDropdownContent } from '@/side-panel/pages/page-layout/components/dropdown-content/FieldWidgetLayoutDropdownContent';
|
||||
import { WidgetSettingsFooter } from '@/side-panel/pages/page-layout/components/WidgetSettingsFooter';
|
||||
import { WidgetSettingsManageSection } from '@/side-panel/pages/page-layout/components/WidgetSettingsManageSection';
|
||||
import { WidgetSettingsPlacementSection } from '@/side-panel/pages/page-layout/components/WidgetSettingsPlacementSection';
|
||||
import { WIDGET_SETTINGS_SELECTABLE_ITEM_IDS } from '@/side-panel/pages/page-layout/constants/settings/WidgetSettingsSelectableItemIds';
|
||||
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
|
||||
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { useWidgetSettingsPlacementSelectableItemIds } from '@/side-panel/pages/page-layout/hooks/useWidgetSettingsPlacementSelectableItemIds';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { styled } from '@linaria/react';
|
||||
@@ -31,10 +34,13 @@ const StyledSidePanelContainer = styled.div`
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export const SidePanelPageLayoutFieldSettings = () => {
|
||||
export const SidePanelRecordPageFieldSettings = () => {
|
||||
const { t } = useLingui();
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStore();
|
||||
|
||||
const { placementSelectableItemIds } =
|
||||
useWidgetSettingsPlacementSelectableItemIds(pageLayoutId);
|
||||
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
const fieldConfiguration = widgetInEditMode?.configuration as
|
||||
@@ -62,7 +68,14 @@ export const SidePanelPageLayoutFieldSettings = () => {
|
||||
? (displayModeLabels[currentDisplayMode] ?? '')
|
||||
: '';
|
||||
|
||||
const selectableItemIds = ['field', 'layout'];
|
||||
const selectableItemIds = [
|
||||
'field',
|
||||
'layout',
|
||||
WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.RESET_TO_DEFAULT,
|
||||
WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.REPLACE_WIDGET,
|
||||
WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.DELETE_WIDGET,
|
||||
...placementSelectableItemIds,
|
||||
];
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
@@ -102,9 +115,10 @@ export const SidePanelPageLayoutFieldSettings = () => {
|
||||
/>
|
||||
</SelectableListItem>
|
||||
</SidePanelGroup>
|
||||
<WidgetSettingsManageSection pageLayoutId={pageLayoutId} />
|
||||
<WidgetSettingsPlacementSection pageLayoutId={pageLayoutId} />
|
||||
</SidePanelList>
|
||||
</StyledSidePanelContainer>
|
||||
<WidgetSettingsFooter pageLayoutId={pageLayoutId} />
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
+15
-70
@@ -1,33 +1,25 @@
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { CommandMenuItemToggle } from '@/command-menu/components/CommandMenuItemToggle';
|
||||
import { useDeletePageLayoutWidget } from '@/page-layout/hooks/useDeletePageLayoutWidget';
|
||||
import { useResetPageLayoutWidgetToDefault } from '@/page-layout/hooks/useResetPageLayoutWidgetToDefault';
|
||||
import { useFieldsWidgetGroups } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetGroups';
|
||||
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
|
||||
import { SidePanelList } from '@/side-panel/components/SidePanelList';
|
||||
import { useSidePanelSubPageHistory } from '@/side-panel/hooks/useSidePanelSubPageHistory';
|
||||
import { NewFieldDefaultVisibilityToggle } from '@/side-panel/pages/page-layout/components/NewFieldDefaultVisibilityToggle';
|
||||
import { WidgetSettingsFooter } from '@/side-panel/pages/page-layout/components/WidgetSettingsFooter';
|
||||
import { WidgetSettingsManageSection } from '@/side-panel/pages/page-layout/components/WidgetSettingsManageSection';
|
||||
import { WidgetSettingsPlacementSection } from '@/side-panel/pages/page-layout/components/WidgetSettingsPlacementSection';
|
||||
import { WIDGET_SETTINGS_SELECTABLE_ITEM_IDS } from '@/side-panel/pages/page-layout/constants/settings/WidgetSettingsSelectableItemIds';
|
||||
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
|
||||
import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
|
||||
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { useWidgetSettingsPlacementSelectableItemIds } from '@/side-panel/pages/page-layout/hooks/useWidgetSettingsPlacementSelectableItemIds';
|
||||
import { SidePanelSubPages } from '@/side-panel/types/SidePanelSubPages';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconLayoutSidebarRight,
|
||||
IconRefreshDot,
|
||||
IconTrash,
|
||||
} from 'twenty-ui/display';
|
||||
import { IconChevronDown, IconLayoutSidebarRight } from 'twenty-ui/display';
|
||||
import { type FieldsConfiguration } from '~/generated-metadata/graphql';
|
||||
|
||||
const RESET_WIDGET_TO_DEFAULT_MODAL_ID = 'reset-widget-to-default-modal';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -41,22 +33,18 @@ const StyledSidePanelContainer = styled.div`
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export const SidePanelPageLayoutFieldsSettings = () => {
|
||||
export const SidePanelRecordPageFieldsSettings = () => {
|
||||
const { t } = useLingui();
|
||||
const { navigateToSidePanelSubPage } = useSidePanelSubPageHistory();
|
||||
const { pageLayoutId, objectNameSingular } =
|
||||
usePageLayoutIdFromContextStore();
|
||||
|
||||
const { placementSelectableItemIds } =
|
||||
useWidgetSettingsPlacementSelectableItemIds(pageLayoutId);
|
||||
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
|
||||
const { deletePageLayoutWidget } = useDeletePageLayoutWidget(pageLayoutId);
|
||||
|
||||
const { resetPageLayoutWidgetToDefault } =
|
||||
useResetPageLayoutWidgetToDefault(pageLayoutId);
|
||||
|
||||
const { openModal } = useModal();
|
||||
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
const fieldsConfiguration = widgetInEditMode?.configuration as
|
||||
@@ -93,30 +81,15 @@ export const SidePanelPageLayoutFieldsSettings = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleResetToDefault = () => {
|
||||
openModal(RESET_WIDGET_TO_DEFAULT_MODAL_ID);
|
||||
};
|
||||
|
||||
const handleConfirmReset = () => {
|
||||
resetPageLayoutWidgetToDefault(widgetInEditMode.id);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
deletePageLayoutWidget(widgetInEditMode.id);
|
||||
};
|
||||
|
||||
const selectableItemIds = [
|
||||
'layout',
|
||||
'new-field-default-visibility',
|
||||
'display-more-fields-button',
|
||||
'action-button',
|
||||
'move-down',
|
||||
'move-up',
|
||||
'move-to-tab',
|
||||
'add-widget-above',
|
||||
'add-widget-below',
|
||||
'reset-to-default',
|
||||
'delete',
|
||||
WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.RESET_TO_DEFAULT,
|
||||
WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.REPLACE_WIDGET,
|
||||
WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.DELETE_WIDGET,
|
||||
...placementSelectableItemIds,
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -155,38 +128,10 @@ export const SidePanelPageLayoutFieldsSettings = () => {
|
||||
widgetId={widgetInEditMode.id}
|
||||
/>
|
||||
</SidePanelGroup>
|
||||
<SidePanelGroup heading={t`Manage`}>
|
||||
<SelectableListItem
|
||||
itemId="reset-to-default"
|
||||
onEnter={handleResetToDefault}
|
||||
>
|
||||
<CommandMenuItem
|
||||
id="reset-to-default"
|
||||
Icon={IconRefreshDot}
|
||||
label={t`Reset to default`}
|
||||
onClick={handleResetToDefault}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
<SelectableListItem itemId="delete" onEnter={handleDelete}>
|
||||
<CommandMenuItem
|
||||
id="delete"
|
||||
Icon={IconTrash}
|
||||
label={t`Delete widget`}
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
</SidePanelGroup>
|
||||
<WidgetSettingsManageSection pageLayoutId={pageLayoutId} />
|
||||
<WidgetSettingsPlacementSection pageLayoutId={pageLayoutId} />
|
||||
</SidePanelList>
|
||||
</StyledSidePanelContainer>
|
||||
<WidgetSettingsFooter pageLayoutId={pageLayoutId} />
|
||||
<ConfirmationModal
|
||||
modalInstanceId={RESET_WIDGET_TO_DEFAULT_MODAL_ID}
|
||||
title={t`Reset to default`}
|
||||
subtitle={t`This will cancel all modifications done on the widget. This action cannot be undone.`}
|
||||
onConfirmClick={handleConfirmReset}
|
||||
confirmButtonText={t`Reset`}
|
||||
confirmButtonAccent="danger"
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
export const WIDGET_SETTINGS_SELECTABLE_ITEM_IDS = {
|
||||
MOVE_DOWN: 'widget-move-down',
|
||||
MOVE_UP: 'widget-move-up',
|
||||
MOVE_TO_TAB: 'widget-move-to-tab',
|
||||
ADD_WIDGET_ABOVE: 'widget-add-above',
|
||||
ADD_WIDGET_BELOW: 'widget-add-below',
|
||||
RESET_TO_DEFAULT: 'widget-reset-to-default',
|
||||
REPLACE_WIDGET: 'widget-replace',
|
||||
DELETE_WIDGET: 'widget-delete',
|
||||
} as const;
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useIsDashboardPageLayout = (): boolean => {
|
||||
const contextStoreCurrentObjectMetadataItemId = useAtomComponentStateValue(
|
||||
contextStoreCurrentObjectMetadataItemIdComponentState,
|
||||
);
|
||||
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
|
||||
|
||||
if (!isDefined(contextStoreCurrentObjectMetadataItemId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === contextStoreCurrentObjectMetadataItemId,
|
||||
);
|
||||
|
||||
return objectMetadataItem?.nameSingular === CoreObjectNameSingular.Dashboard;
|
||||
};
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { useCanMovePageLayoutWidgetDown } from '@/page-layout/hooks/useCanMovePageLayoutWidgetDown';
|
||||
import { useCanMovePageLayoutWidgetUp } from '@/page-layout/hooks/useCanMovePageLayoutWidgetUp';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { WIDGET_SETTINGS_SELECTABLE_ITEM_IDS } from '@/side-panel/pages/page-layout/constants/settings/WidgetSettingsSelectableItemIds';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useWidgetSettingsPlacementSelectableItemIds = (
|
||||
pageLayoutId: string,
|
||||
) => {
|
||||
const pageLayoutEditingWidgetId = useAtomComponentStateValue(
|
||||
pageLayoutEditingWidgetIdComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const { canMovePageLayoutWidgetUp } =
|
||||
useCanMovePageLayoutWidgetUp(pageLayoutId);
|
||||
const { canMovePageLayoutWidgetDown } =
|
||||
useCanMovePageLayoutWidgetDown(pageLayoutId);
|
||||
|
||||
const showMoveUp =
|
||||
isDefined(pageLayoutEditingWidgetId) &&
|
||||
canMovePageLayoutWidgetUp(pageLayoutEditingWidgetId);
|
||||
|
||||
const showMoveDown =
|
||||
isDefined(pageLayoutEditingWidgetId) &&
|
||||
canMovePageLayoutWidgetDown(pageLayoutEditingWidgetId);
|
||||
|
||||
const placementSelectableItemIds = [
|
||||
...(showMoveUp ? [WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_UP] : []),
|
||||
...(showMoveDown ? [WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_DOWN] : []),
|
||||
WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_TO_TAB,
|
||||
WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.ADD_WIDGET_ABOVE,
|
||||
WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.ADD_WIDGET_BELOW,
|
||||
];
|
||||
|
||||
return { placementSelectableItemIds };
|
||||
};
|
||||
+7
-6
@@ -1,10 +1,11 @@
|
||||
import { type SidePanelPages } from 'twenty-shared/types';
|
||||
|
||||
export type PageLayoutSidePanelPage =
|
||||
| SidePanelPages.PageLayoutWidgetTypeSelect
|
||||
| SidePanelPages.PageLayoutGraphTypeSelect
|
||||
| SidePanelPages.PageLayoutIframeSettings
|
||||
| SidePanelPages.PageLayoutDashboardWidgetTypeSelect
|
||||
| SidePanelPages.PageLayoutTabSettings
|
||||
| SidePanelPages.PageLayoutFieldsSettings
|
||||
| SidePanelPages.PageLayoutFieldSettings
|
||||
| SidePanelPages.PageLayoutRecordTableSettings;
|
||||
| SidePanelPages.DashboardChartSettings
|
||||
| SidePanelPages.DashboardIframeSettings
|
||||
| SidePanelPages.DashboardRecordTableSettings
|
||||
| SidePanelPages.RecordPageFieldsSettings
|
||||
| SidePanelPages.RecordPageFieldSettings
|
||||
| SidePanelPages.PageLayoutRecordPageWidgetTypeSelect;
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { isPageLayoutSidePanelPage } from '@/side-panel/pages/page-layout/utils/isPageLayoutSidePanelPage';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
|
||||
describe('isPageLayoutSidePanelPage', () => {
|
||||
const pageLayoutPages: SidePanelPages[] = [
|
||||
SidePanelPages.PageLayoutDashboardWidgetTypeSelect,
|
||||
SidePanelPages.PageLayoutTabSettings,
|
||||
SidePanelPages.DashboardChartSettings,
|
||||
SidePanelPages.DashboardIframeSettings,
|
||||
SidePanelPages.DashboardRecordTableSettings,
|
||||
SidePanelPages.RecordPageFieldsSettings,
|
||||
SidePanelPages.RecordPageFieldSettings,
|
||||
SidePanelPages.PageLayoutRecordPageWidgetTypeSelect,
|
||||
];
|
||||
|
||||
it.each(pageLayoutPages)(
|
||||
'should return true for page layout page: %s',
|
||||
(page) => {
|
||||
expect(isPageLayoutSidePanelPage(page)).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
const nonPageLayoutPages: SidePanelPages[] = [
|
||||
SidePanelPages.ViewRecord,
|
||||
SidePanelPages.AskAI,
|
||||
SidePanelPages.ComposeEmail,
|
||||
SidePanelPages.SearchRecords,
|
||||
SidePanelPages.ViewFrontComponent,
|
||||
];
|
||||
|
||||
it.each(nonPageLayoutPages)(
|
||||
'should return false for non-page-layout page: %s',
|
||||
(page) => {
|
||||
expect(isPageLayoutSidePanelPage(page)).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
+9
-6
@@ -6,25 +6,28 @@ import {
|
||||
IconChartPie,
|
||||
IconFrame,
|
||||
IconList,
|
||||
IconPlus,
|
||||
IconTable,
|
||||
} from 'twenty-ui/display';
|
||||
|
||||
export const getPageLayoutIcon = (page: PageLayoutSidePanelPage) => {
|
||||
switch (page) {
|
||||
case SidePanelPages.PageLayoutWidgetTypeSelect:
|
||||
case SidePanelPages.PageLayoutDashboardWidgetTypeSelect:
|
||||
return IconAppWindow;
|
||||
case SidePanelPages.PageLayoutGraphTypeSelect:
|
||||
case SidePanelPages.DashboardChartSettings:
|
||||
return IconChartPie;
|
||||
case SidePanelPages.PageLayoutIframeSettings:
|
||||
case SidePanelPages.DashboardIframeSettings:
|
||||
return IconFrame;
|
||||
case SidePanelPages.PageLayoutTabSettings:
|
||||
return IconAppWindow;
|
||||
case SidePanelPages.PageLayoutFieldsSettings:
|
||||
case SidePanelPages.RecordPageFieldsSettings:
|
||||
return IconList;
|
||||
case SidePanelPages.PageLayoutFieldSettings:
|
||||
case SidePanelPages.RecordPageFieldSettings:
|
||||
return IconList;
|
||||
case SidePanelPages.PageLayoutRecordTableSettings:
|
||||
case SidePanelPages.DashboardRecordTableSettings:
|
||||
return IconTable;
|
||||
case SidePanelPages.PageLayoutRecordPageWidgetTypeSelect:
|
||||
return IconPlus;
|
||||
default:
|
||||
assertUnreachable(page);
|
||||
}
|
||||
|
||||
+8
-6
@@ -5,20 +5,22 @@ import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
export const getPageLayoutPageTitle = (page: PageLayoutSidePanelPage) => {
|
||||
switch (page) {
|
||||
case SidePanelPages.PageLayoutWidgetTypeSelect:
|
||||
case SidePanelPages.PageLayoutDashboardWidgetTypeSelect:
|
||||
return t`Add Widget`;
|
||||
case SidePanelPages.PageLayoutGraphTypeSelect:
|
||||
case SidePanelPages.DashboardChartSettings:
|
||||
return t`Select Graph Type`;
|
||||
case SidePanelPages.PageLayoutIframeSettings:
|
||||
case SidePanelPages.DashboardIframeSettings:
|
||||
return t`iFrame Settings`;
|
||||
case SidePanelPages.PageLayoutTabSettings:
|
||||
return t`Tab Settings`;
|
||||
case SidePanelPages.PageLayoutFieldsSettings:
|
||||
case SidePanelPages.RecordPageFieldsSettings:
|
||||
return t`Fields Settings`;
|
||||
case SidePanelPages.PageLayoutFieldSettings:
|
||||
case SidePanelPages.RecordPageFieldSettings:
|
||||
return t`Field widget`;
|
||||
case SidePanelPages.PageLayoutRecordTableSettings:
|
||||
case SidePanelPages.DashboardRecordTableSettings:
|
||||
return t`Record Table Settings`;
|
||||
case SidePanelPages.PageLayoutRecordPageWidgetTypeSelect:
|
||||
return t`New widget`;
|
||||
default:
|
||||
assertUnreachable(page);
|
||||
}
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
|
||||
import { type PageLayoutSidePanelPage } from '@/side-panel/pages/page-layout/types/PageLayoutSidePanelPage';
|
||||
|
||||
const PAGE_LAYOUT_SIDE_PANEL_PAGES: PageLayoutSidePanelPage[] = [
|
||||
SidePanelPages.PageLayoutDashboardWidgetTypeSelect,
|
||||
SidePanelPages.PageLayoutTabSettings,
|
||||
SidePanelPages.DashboardChartSettings,
|
||||
SidePanelPages.DashboardIframeSettings,
|
||||
SidePanelPages.DashboardRecordTableSettings,
|
||||
SidePanelPages.RecordPageFieldsSettings,
|
||||
SidePanelPages.RecordPageFieldSettings,
|
||||
SidePanelPages.PageLayoutRecordPageWidgetTypeSelect,
|
||||
];
|
||||
|
||||
export const isPageLayoutSidePanelPage = (
|
||||
page: SidePanelPages,
|
||||
): page is PageLayoutSidePanelPage => {
|
||||
return (PAGE_LAYOUT_SIDE_PANEL_PAGES as SidePanelPages[]).includes(page);
|
||||
};
|
||||
@@ -15,16 +15,17 @@ export enum SidePanelPages {
|
||||
SearchRecords = 'search-records',
|
||||
AskAI = 'ask-ai',
|
||||
ViewPreviousAIChats = 'view-previous-ai-chats',
|
||||
PageLayoutWidgetTypeSelect = 'page-layout-widget-type-select',
|
||||
PageLayoutGraphTypeSelect = 'page-layout-graph-type-select',
|
||||
PageLayoutIframeSettings = 'page-layout-iframe-settings',
|
||||
PageLayoutDashboardWidgetTypeSelect = 'page-layout-dashboard-widget-type-select',
|
||||
PageLayoutTabSettings = 'page-layout-tab-settings',
|
||||
PageLayoutFieldsSettings = 'page-layout-fields-settings',
|
||||
PageLayoutRecordTableSettings = 'page-layout-record-table-settings',
|
||||
PageLayoutFieldSettings = 'page-layout-field-settings',
|
||||
DashboardChartSettings = 'dashboard-chart-settings',
|
||||
DashboardIframeSettings = 'dashboard-iframe-settings',
|
||||
DashboardRecordTableSettings = 'dashboard-record-table-settings',
|
||||
RecordPageFieldsSettings = 'record-page-fields-settings',
|
||||
RecordPageFieldSettings = 'record-page-field-settings',
|
||||
ViewFrontComponent = 'view-front-component',
|
||||
NavigationMenuItemEdit = 'navigation-menu-item-edit',
|
||||
NavigationMenuAddItem = 'navigation-menu-add-item',
|
||||
CommandMenuEdit = 'command-menu-edit',
|
||||
PageLayoutRecordPageWidgetTypeSelect = 'page-layout-record-page-widget-type-select',
|
||||
ComposeEmail = 'compose-email',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user