fix(front): isolate record table dashboard widget filters on duplicate (#21936)
closes https://discord.com/channels/1130383047699738754/1518291134382608394 https://github.com/user-attachments/assets/931e4e88-44e8-4634-a7f9-e0564bd80fff <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21936?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
+300
@@ -0,0 +1,300 @@
|
||||
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
|
||||
import { useDuplicatePageLayoutTab } from '@/page-layout/hooks/useDuplicatePageLayoutTab';
|
||||
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
|
||||
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
|
||||
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { type RecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewSnapshot';
|
||||
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { createStore } from 'jotai';
|
||||
import { type ReactNode } from 'react';
|
||||
import {
|
||||
PageLayoutTabLayoutMode,
|
||||
PageLayoutType,
|
||||
ViewOpenRecordIn,
|
||||
ViewType,
|
||||
ViewVisibility,
|
||||
WidgetConfigurationType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
import {
|
||||
PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
PageLayoutTestWrapper,
|
||||
} from './PageLayoutTestWrapper';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@/side-panel/hooks/useSidePanelMenu', () => ({
|
||||
useSidePanelMenu: () => ({
|
||||
closeSidePanelMenu: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel',
|
||||
() => ({
|
||||
useNavigatePageLayoutSidePanel: () => ({
|
||||
navigatePageLayoutSidePanel: jest.fn(),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const SOURCE_TAB_ID = 'source-tab-id';
|
||||
const SOURCE_WIDGET_ID = 'source-widget-id';
|
||||
const SOURCE_VIEW_ID = 'source-view-id';
|
||||
|
||||
const makeRecordTableWidget = (): PageLayoutWidget =>
|
||||
({
|
||||
__typename: 'PageLayoutWidget',
|
||||
id: SOURCE_WIDGET_ID,
|
||||
applicationId: 'application-id',
|
||||
pageLayoutTabId: SOURCE_TAB_ID,
|
||||
title: 'Companies',
|
||||
type: WidgetType.RECORD_TABLE,
|
||||
objectMetadataId: 'object-metadata-id',
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 },
|
||||
position: {
|
||||
__typename: 'PageLayoutWidgetGridPosition',
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 4,
|
||||
columnSpan: 4,
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'RecordTableConfiguration',
|
||||
configurationType: WidgetConfigurationType.RECORD_TABLE,
|
||||
viewId: SOURCE_VIEW_ID,
|
||||
},
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
deletedAt: null,
|
||||
isActive: true,
|
||||
}) as PageLayoutWidget;
|
||||
|
||||
const makeSourceTab = (widgets: PageLayoutWidget[]): PageLayoutTab =>
|
||||
({
|
||||
__typename: 'PageLayoutTab',
|
||||
id: SOURCE_TAB_ID,
|
||||
applicationId: 'application-id',
|
||||
title: 'Companies',
|
||||
icon: null,
|
||||
position: 0,
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
pageLayoutId: PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
widgets,
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
deletedAt: null,
|
||||
isActive: true,
|
||||
}) as PageLayoutTab;
|
||||
|
||||
const makeDraftPageLayout = (tabs: PageLayoutTab[]): DraftPageLayout => ({
|
||||
id: PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
name: 'Test Layout',
|
||||
type: PageLayoutType.DASHBOARD,
|
||||
objectMetadataId: null,
|
||||
tabs,
|
||||
});
|
||||
|
||||
const sourceRecordTableViewSnapshot: RecordTableWidgetViewSnapshot = {
|
||||
view: {
|
||||
id: SOURCE_VIEW_ID,
|
||||
name: 'Companies Table',
|
||||
icon: 'IconTable',
|
||||
objectMetadataId: 'object-metadata-id',
|
||||
type: ViewType.TABLE_WIDGET,
|
||||
isCompact: false,
|
||||
position: 0,
|
||||
openRecordIn: ViewOpenRecordIn.RECORD_PAGE,
|
||||
visibility: ViewVisibility.WORKSPACE,
|
||||
shouldHideEmptyGroups: false,
|
||||
isActive: true,
|
||||
},
|
||||
viewFields: [
|
||||
{
|
||||
id: 'source-view-field-id',
|
||||
viewId: SOURCE_VIEW_ID,
|
||||
fieldMetadataId: 'field-metadata-id',
|
||||
position: 0,
|
||||
size: 180,
|
||||
isVisible: true,
|
||||
isActive: true,
|
||||
},
|
||||
],
|
||||
viewFilterGroups: [],
|
||||
viewFilters: [],
|
||||
viewSorts: [],
|
||||
};
|
||||
|
||||
describe('useDuplicatePageLayoutTab', () => {
|
||||
const getWrapper =
|
||||
(store = createStore()) =>
|
||||
({ children }: { children: ReactNode }) => (
|
||||
<PageLayoutTestWrapper
|
||||
store={store}
|
||||
instanceId={PAGE_LAYOUT_TEST_INSTANCE_ID}
|
||||
>
|
||||
{children}
|
||||
</PageLayoutTestWrapper>
|
||||
);
|
||||
|
||||
const getPageLayoutDraftAtom = () =>
|
||||
pageLayoutDraftComponentState.atomFamily({
|
||||
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
});
|
||||
|
||||
const getPageLayoutCurrentLayoutsAtom = () =>
|
||||
pageLayoutCurrentLayoutsComponentState.atomFamily({
|
||||
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
});
|
||||
|
||||
const getRecordTableWidgetViewDraftAtom = () =>
|
||||
recordTableWidgetViewDraftComponentState.atomFamily({
|
||||
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should clone record-table widget views when duplicating a tab', () => {
|
||||
const uuidModule = require('uuid');
|
||||
uuidModule.v4
|
||||
.mockReturnValueOnce('new-tab-id')
|
||||
.mockReturnValueOnce('new-widget-id')
|
||||
.mockReturnValueOnce('new-view-id')
|
||||
.mockReturnValueOnce('new-view-field-id');
|
||||
|
||||
const store = createStore();
|
||||
const recordTableWidget = makeRecordTableWidget();
|
||||
|
||||
store.set(
|
||||
getPageLayoutDraftAtom(),
|
||||
makeDraftPageLayout([makeSourceTab([recordTableWidget])]),
|
||||
);
|
||||
store.set(getPageLayoutCurrentLayoutsAtom(), {
|
||||
[SOURCE_TAB_ID]: {
|
||||
desktop: [{ i: SOURCE_WIDGET_ID, x: 0, y: 0, w: 4, h: 4 }],
|
||||
mobile: [{ i: SOURCE_WIDGET_ID, x: 0, y: 0, w: 1, h: 4 }],
|
||||
},
|
||||
});
|
||||
store.set(getRecordTableWidgetViewDraftAtom(), {
|
||||
[SOURCE_WIDGET_ID]: sourceRecordTableViewSnapshot,
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useDuplicatePageLayoutTab({
|
||||
pageLayoutId: PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
tabListInstanceId: getTabListInstanceIdFromPageLayoutId(
|
||||
PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
),
|
||||
}),
|
||||
{ wrapper: getWrapper(store) },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.duplicateTab(SOURCE_TAB_ID);
|
||||
});
|
||||
|
||||
const pageLayoutDraft = store.get(getPageLayoutDraftAtom());
|
||||
const duplicatedTab = pageLayoutDraft.tabs.find(
|
||||
(tab) => tab.id === 'new-tab-id',
|
||||
);
|
||||
const duplicatedWidget = duplicatedTab?.widgets[0];
|
||||
const recordTableWidgetViewDraft = store.get(
|
||||
getRecordTableWidgetViewDraftAtom(),
|
||||
);
|
||||
|
||||
expect(duplicatedWidget?.id).toBe('new-widget-id');
|
||||
expect(duplicatedWidget?.configuration).toMatchObject({
|
||||
viewId: 'new-view-id',
|
||||
});
|
||||
expect(recordTableWidgetViewDraft['new-widget-id'].view.id).toBe(
|
||||
'new-view-id',
|
||||
);
|
||||
expect(recordTableWidgetViewDraft['new-widget-id'].viewFields[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: 'new-view-field-id',
|
||||
viewId: 'new-view-id',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should clone record-table widget views from metadata when source draft is not initialized', () => {
|
||||
const uuidModule = require('uuid');
|
||||
uuidModule.v4
|
||||
.mockReturnValueOnce('new-tab-id')
|
||||
.mockReturnValueOnce('new-widget-id')
|
||||
.mockReturnValueOnce('new-view-id')
|
||||
.mockReturnValueOnce('new-view-field-id');
|
||||
|
||||
const store = createStore();
|
||||
const recordTableWidget = makeRecordTableWidget();
|
||||
|
||||
store.set(
|
||||
getPageLayoutDraftAtom(),
|
||||
makeDraftPageLayout([makeSourceTab([recordTableWidget])]),
|
||||
);
|
||||
store.set(getPageLayoutCurrentLayoutsAtom(), {
|
||||
[SOURCE_TAB_ID]: {
|
||||
desktop: [{ i: SOURCE_WIDGET_ID, x: 0, y: 0, w: 4, h: 4 }],
|
||||
mobile: [{ i: SOURCE_WIDGET_ID, x: 0, y: 0, w: 1, h: 4 }],
|
||||
},
|
||||
});
|
||||
store.set(metadataStoreState.atomFamily('views'), {
|
||||
current: [sourceRecordTableViewSnapshot.view],
|
||||
draft: [],
|
||||
status: 'up-to-date',
|
||||
});
|
||||
store.set(metadataStoreState.atomFamily('viewFields'), {
|
||||
current: sourceRecordTableViewSnapshot.viewFields,
|
||||
draft: [],
|
||||
status: 'up-to-date',
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useDuplicatePageLayoutTab({
|
||||
pageLayoutId: PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
tabListInstanceId: getTabListInstanceIdFromPageLayoutId(
|
||||
PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
),
|
||||
}),
|
||||
{ wrapper: getWrapper(store) },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.duplicateTab(SOURCE_TAB_ID);
|
||||
});
|
||||
|
||||
const pageLayoutDraft = store.get(getPageLayoutDraftAtom());
|
||||
const duplicatedTab = pageLayoutDraft.tabs.find(
|
||||
(tab) => tab.id === 'new-tab-id',
|
||||
);
|
||||
const duplicatedWidget = duplicatedTab?.widgets[0];
|
||||
const recordTableWidgetViewDraft = store.get(
|
||||
getRecordTableWidgetViewDraftAtom(),
|
||||
);
|
||||
|
||||
expect(duplicatedWidget?.configuration).toMatchObject({
|
||||
viewId: 'new-view-id',
|
||||
});
|
||||
expect(recordTableWidgetViewDraft['new-widget-id'].view.id).toBe(
|
||||
'new-view-id',
|
||||
);
|
||||
expect(recordTableWidgetViewDraft['new-widget-id'].viewFields[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: 'new-view-field-id',
|
||||
viewId: 'new-view-id',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useDuplicateFieldsWidgetForPageLayout } from '@/page-layout/hooks/useDuplicateFieldsWidgetForPageLayout';
|
||||
import { useDuplicateRecordTableWidgetForPageLayout } from '@/page-layout/hooks/useDuplicateRecordTableWidgetForPageLayout';
|
||||
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
|
||||
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
@@ -62,6 +63,11 @@ export const useDuplicatePageLayoutTab = ({
|
||||
pageLayoutId,
|
||||
});
|
||||
|
||||
const { duplicateRecordTableWidget } =
|
||||
useDuplicateRecordTableWidgetForPageLayout({
|
||||
pageLayoutId,
|
||||
});
|
||||
|
||||
const duplicateTab = useCallback(
|
||||
(tabId: string): string => {
|
||||
const currentPageLayoutDraft = store.get(pageLayoutDraft);
|
||||
@@ -84,15 +90,20 @@ export const useDuplicatePageLayoutTab = ({
|
||||
const newWidgetId = uuidv4();
|
||||
widgetOldIdNewIdMap.set(widget.id, newWidgetId);
|
||||
|
||||
const fieldsWidgetCopyResult = duplicateFieldsWidget({
|
||||
sourceWidget: widget,
|
||||
newWidgetId,
|
||||
});
|
||||
const widgetViewCopyResult =
|
||||
duplicateFieldsWidget({
|
||||
sourceWidget: widget,
|
||||
newWidgetId,
|
||||
}) ??
|
||||
duplicateRecordTableWidget({
|
||||
sourceWidget: widget,
|
||||
newWidgetId,
|
||||
});
|
||||
|
||||
const clonedConfiguration = isDefined(fieldsWidgetCopyResult)
|
||||
const clonedConfiguration = isDefined(widgetViewCopyResult)
|
||||
? {
|
||||
...widget.configuration,
|
||||
viewId: fieldsWidgetCopyResult.newViewId,
|
||||
viewId: widgetViewCopyResult.newViewId,
|
||||
}
|
||||
: widget.configuration;
|
||||
|
||||
@@ -168,6 +179,7 @@ export const useDuplicatePageLayoutTab = ({
|
||||
[
|
||||
closeSidePanelMenu,
|
||||
duplicateFieldsWidget,
|
||||
duplicateRecordTableWidget,
|
||||
navigatePageLayoutSidePanel,
|
||||
pageLayoutCurrentLayouts,
|
||||
pageLayoutDraft,
|
||||
|
||||
+12
-6
@@ -1,4 +1,5 @@
|
||||
import { useDuplicateFieldsWidgetForPageLayout } from '@/page-layout/hooks/useDuplicateFieldsWidgetForPageLayout';
|
||||
import { useDuplicateRecordTableWidgetForPageLayout } from '@/page-layout/hooks/useDuplicateRecordTableWidgetForPageLayout';
|
||||
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
|
||||
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
@@ -50,6 +51,11 @@ export const useDuplicatePageLayoutWidget = (
|
||||
pageLayoutId,
|
||||
});
|
||||
|
||||
const { duplicateRecordTableWidget } =
|
||||
useDuplicateRecordTableWidgetForPageLayout({
|
||||
pageLayoutId,
|
||||
});
|
||||
|
||||
const duplicateWidget = useCallback(
|
||||
(widgetId: string): string => {
|
||||
const pageLayoutDraft = store.get(pageLayoutDraftState);
|
||||
@@ -76,15 +82,14 @@ export const useDuplicatePageLayoutWidget = (
|
||||
|
||||
const newWidgetId = uuidv4();
|
||||
|
||||
const fieldsWidgetCopyResult = duplicateFieldsWidget({
|
||||
sourceWidget,
|
||||
newWidgetId,
|
||||
});
|
||||
const widgetViewCopyResult =
|
||||
duplicateFieldsWidget({ sourceWidget, newWidgetId }) ??
|
||||
duplicateRecordTableWidget({ sourceWidget, newWidgetId });
|
||||
|
||||
const clonedConfiguration = isDefined(fieldsWidgetCopyResult)
|
||||
const clonedConfiguration = isDefined(widgetViewCopyResult)
|
||||
? {
|
||||
...sourceWidget.configuration,
|
||||
viewId: fieldsWidgetCopyResult.newViewId,
|
||||
viewId: widgetViewCopyResult.newViewId,
|
||||
}
|
||||
: sourceWidget.configuration;
|
||||
|
||||
@@ -156,6 +161,7 @@ export const useDuplicatePageLayoutWidget = (
|
||||
},
|
||||
[
|
||||
duplicateFieldsWidget,
|
||||
duplicateRecordTableWidget,
|
||||
pageLayoutCurrentLayoutsState,
|
||||
pageLayoutDraftState,
|
||||
setPageLayoutEditingWidgetId,
|
||||
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { getWidgetConfigurationViewId } from '@/page-layout/utils/getWidgetConfigurationViewId';
|
||||
import { buildRecordTableWidgetViewSnapshotFromView } from '@/page-layout/widgets/record-table/utils/buildRecordTableWidgetViewSnapshotFromView';
|
||||
import { cloneRecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/utils/cloneRecordTableWidgetViewSnapshot';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { viewFromViewIdFamilySelector } from '@/views/states/selectors/viewFromViewIdFamilySelector';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WidgetType } from '~/generated-metadata/graphql';
|
||||
|
||||
type DuplicateRecordTableWidgetParams = {
|
||||
sourceWidget: PageLayoutWidget;
|
||||
newWidgetId: string;
|
||||
};
|
||||
|
||||
type DuplicateRecordTableWidgetResult = {
|
||||
newViewId: string;
|
||||
};
|
||||
|
||||
export const useDuplicateRecordTableWidgetForPageLayout = ({
|
||||
pageLayoutId,
|
||||
}: {
|
||||
pageLayoutId: string;
|
||||
}) => {
|
||||
const store = useStore();
|
||||
|
||||
const recordTableWidgetViewDraftState = useAtomComponentStateCallbackState(
|
||||
recordTableWidgetViewDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const duplicateRecordTableWidget = useCallback(
|
||||
({
|
||||
sourceWidget,
|
||||
newWidgetId,
|
||||
}: DuplicateRecordTableWidgetParams): DuplicateRecordTableWidgetResult | null => {
|
||||
if (sourceWidget.type !== WidgetType.RECORD_TABLE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sourceSnapshotFromDraft = store.get(
|
||||
recordTableWidgetViewDraftState,
|
||||
)[sourceWidget.id];
|
||||
|
||||
const sourceViewId = getWidgetConfigurationViewId(
|
||||
sourceWidget.configuration,
|
||||
);
|
||||
|
||||
const sourceViewFromMetadataStore =
|
||||
!isDefined(sourceSnapshotFromDraft) && isDefined(sourceViewId)
|
||||
? store.get(
|
||||
viewFromViewIdFamilySelector.selectorFamily({
|
||||
viewId: sourceViewId,
|
||||
}),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const sourceSnapshotFromMetadataStore = isDefined(
|
||||
sourceViewFromMetadataStore,
|
||||
)
|
||||
? buildRecordTableWidgetViewSnapshotFromView(
|
||||
sourceViewFromMetadataStore,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const snapshotToClone =
|
||||
sourceSnapshotFromDraft ?? sourceSnapshotFromMetadataStore;
|
||||
|
||||
if (!isDefined(snapshotToClone)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const clonedSnapshot =
|
||||
cloneRecordTableWidgetViewSnapshot(snapshotToClone);
|
||||
|
||||
store.set(recordTableWidgetViewDraftState, (previousDraft) => ({
|
||||
...previousDraft,
|
||||
[newWidgetId]: clonedSnapshot,
|
||||
}));
|
||||
|
||||
return { newViewId: clonedSnapshot.view.id };
|
||||
},
|
||||
[recordTableWidgetViewDraftState, store],
|
||||
);
|
||||
|
||||
return { duplicateRecordTableWidget };
|
||||
};
|
||||
+2
-43
@@ -1,10 +1,6 @@
|
||||
import { type FlatViewField } from '@/metadata-store/types/FlatViewField';
|
||||
import { type FlatViewFilter } from '@/metadata-store/types/FlatViewFilter';
|
||||
import { type FlatViewFilterGroup } from '@/metadata-store/types/FlatViewFilterGroup';
|
||||
import { type FlatViewSort } from '@/metadata-store/types/FlatViewSort';
|
||||
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
|
||||
import { recordTableWidgetViewPersistedComponentState } from '@/page-layout/states/recordTableWidgetViewPersistedComponentState';
|
||||
import { type RecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewSnapshot';
|
||||
import { buildRecordTableWidgetViewSnapshotFromView } from '@/page-layout/widgets/record-table/utils/buildRecordTableWidgetViewSnapshotFromView';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { type View } from '@/views/types/View';
|
||||
import { useStore } from 'jotai';
|
||||
@@ -41,44 +37,7 @@ export const useInitializeRecordTableWidgetViewDraft = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
viewFields,
|
||||
viewFilters,
|
||||
viewFilterGroups,
|
||||
viewSorts,
|
||||
viewGroups: _viewGroups,
|
||||
...viewProps
|
||||
} = view;
|
||||
|
||||
const flatViewFields: FlatViewField[] = viewFields.map((field) => ({
|
||||
...field,
|
||||
viewId: view.id,
|
||||
}));
|
||||
|
||||
const flatViewFilters: FlatViewFilter[] = viewFilters.map((filter) => ({
|
||||
...filter,
|
||||
viewId: view.id,
|
||||
}));
|
||||
|
||||
const flatViewFilterGroups: FlatViewFilterGroup[] = (
|
||||
viewFilterGroups ?? []
|
||||
).map((group) => ({
|
||||
...group,
|
||||
viewId: view.id,
|
||||
}));
|
||||
|
||||
const flatViewSorts: FlatViewSort[] = viewSorts.map((sort) => ({
|
||||
...sort,
|
||||
viewId: view.id,
|
||||
}));
|
||||
|
||||
const snapshot: RecordTableWidgetViewSnapshot = {
|
||||
view: viewProps,
|
||||
viewFields: flatViewFields,
|
||||
viewFilters: flatViewFilters,
|
||||
viewFilterGroups: flatViewFilterGroups,
|
||||
viewSorts: flatViewSorts,
|
||||
};
|
||||
const snapshot = buildRecordTableWidgetViewSnapshotFromView(view);
|
||||
|
||||
store.set(recordTableWidgetViewDraftState, (prev) => ({
|
||||
...prev,
|
||||
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
import { type RecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewSnapshot';
|
||||
import { cloneRecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/utils/cloneRecordTableWidgetViewSnapshot';
|
||||
import { ViewFilterOperand } from 'twenty-shared/types';
|
||||
import {
|
||||
ViewFilterGroupLogicalOperator,
|
||||
ViewOpenRecordIn,
|
||||
ViewSortDirection,
|
||||
ViewType,
|
||||
ViewVisibility,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const SOURCE_VIEW_ID = 'source-view-id';
|
||||
const PARENT_FILTER_GROUP_ID = 'parent-filter-group-id';
|
||||
const CHILD_FILTER_GROUP_ID = 'child-filter-group-id';
|
||||
|
||||
const sourceSnapshot: RecordTableWidgetViewSnapshot = {
|
||||
view: {
|
||||
id: SOURCE_VIEW_ID,
|
||||
name: 'Companies Table',
|
||||
icon: 'IconTable',
|
||||
objectMetadataId: 'object-metadata-id',
|
||||
type: ViewType.TABLE_WIDGET,
|
||||
isCompact: false,
|
||||
position: 0,
|
||||
openRecordIn: ViewOpenRecordIn.RECORD_PAGE,
|
||||
visibility: ViewVisibility.WORKSPACE,
|
||||
shouldHideEmptyGroups: false,
|
||||
isActive: true,
|
||||
},
|
||||
viewFields: [
|
||||
{
|
||||
id: 'view-field-id',
|
||||
viewId: SOURCE_VIEW_ID,
|
||||
fieldMetadataId: 'field-metadata-id',
|
||||
position: 0,
|
||||
size: 180,
|
||||
isVisible: true,
|
||||
isActive: true,
|
||||
},
|
||||
],
|
||||
viewFilterGroups: [
|
||||
{
|
||||
id: PARENT_FILTER_GROUP_ID,
|
||||
viewId: SOURCE_VIEW_ID,
|
||||
logicalOperator: ViewFilterGroupLogicalOperator.AND,
|
||||
parentViewFilterGroupId: null,
|
||||
positionInViewFilterGroup: null,
|
||||
},
|
||||
{
|
||||
id: CHILD_FILTER_GROUP_ID,
|
||||
viewId: SOURCE_VIEW_ID,
|
||||
logicalOperator: ViewFilterGroupLogicalOperator.OR,
|
||||
parentViewFilterGroupId: PARENT_FILTER_GROUP_ID,
|
||||
positionInViewFilterGroup: 0,
|
||||
},
|
||||
],
|
||||
viewFilters: [
|
||||
{
|
||||
id: 'view-filter-id',
|
||||
viewId: SOURCE_VIEW_ID,
|
||||
fieldMetadataId: 'field-metadata-id',
|
||||
operand: ViewFilterOperand.IS,
|
||||
value: 'Acme',
|
||||
viewFilterGroupId: CHILD_FILTER_GROUP_ID,
|
||||
positionInViewFilterGroup: 0,
|
||||
subFieldName: null,
|
||||
},
|
||||
],
|
||||
viewSorts: [
|
||||
{
|
||||
id: 'view-sort-id',
|
||||
viewId: SOURCE_VIEW_ID,
|
||||
fieldMetadataId: 'field-metadata-id',
|
||||
direction: ViewSortDirection.ASC,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('cloneRecordTableWidgetViewSnapshot', () => {
|
||||
it('should assign a new view id and re-point every nested viewId to it', () => {
|
||||
const clonedSnapshot = cloneRecordTableWidgetViewSnapshot(sourceSnapshot);
|
||||
|
||||
expect(clonedSnapshot.view.id).not.toBe(SOURCE_VIEW_ID);
|
||||
|
||||
const newViewId = clonedSnapshot.view.id;
|
||||
|
||||
expect(clonedSnapshot.viewFields[0].viewId).toBe(newViewId);
|
||||
expect(clonedSnapshot.viewFilterGroups[0].viewId).toBe(newViewId);
|
||||
expect(clonedSnapshot.viewFilters[0].viewId).toBe(newViewId);
|
||||
expect(clonedSnapshot.viewSorts[0].viewId).toBe(newViewId);
|
||||
});
|
||||
|
||||
it('should regenerate row ids so the duplicate persists without colliding with the source', () => {
|
||||
const clonedSnapshot = cloneRecordTableWidgetViewSnapshot(sourceSnapshot);
|
||||
|
||||
expect(clonedSnapshot.viewFields[0].id).not.toBe('view-field-id');
|
||||
expect(clonedSnapshot.viewFilters[0].id).not.toBe('view-filter-id');
|
||||
expect(clonedSnapshot.viewSorts[0].id).not.toBe('view-sort-id');
|
||||
expect(clonedSnapshot.viewFilterGroups[0].id).not.toBe(
|
||||
PARENT_FILTER_GROUP_ID,
|
||||
);
|
||||
expect(clonedSnapshot.viewFilterGroups[1].id).not.toBe(
|
||||
CHILD_FILTER_GROUP_ID,
|
||||
);
|
||||
});
|
||||
|
||||
it('should re-point filter-group references to the regenerated group ids', () => {
|
||||
const clonedSnapshot = cloneRecordTableWidgetViewSnapshot(sourceSnapshot);
|
||||
|
||||
const [parentGroup, childGroup] = clonedSnapshot.viewFilterGroups;
|
||||
|
||||
expect(childGroup.parentViewFilterGroupId).toBe(parentGroup.id);
|
||||
expect(clonedSnapshot.viewFilters[0].viewFilterGroupId).toBe(childGroup.id);
|
||||
});
|
||||
|
||||
it('should preserve filter, sort and field content', () => {
|
||||
const clonedSnapshot = cloneRecordTableWidgetViewSnapshot(sourceSnapshot);
|
||||
|
||||
expect(clonedSnapshot.viewFilters[0]).toMatchObject({
|
||||
fieldMetadataId: 'field-metadata-id',
|
||||
operand: ViewFilterOperand.IS,
|
||||
value: 'Acme',
|
||||
});
|
||||
expect(clonedSnapshot.viewSorts[0]).toMatchObject({
|
||||
fieldMetadataId: 'field-metadata-id',
|
||||
direction: ViewSortDirection.ASC,
|
||||
});
|
||||
expect(clonedSnapshot.viewFields[0]).toMatchObject({
|
||||
fieldMetadataId: 'field-metadata-id',
|
||||
position: 0,
|
||||
size: 180,
|
||||
});
|
||||
});
|
||||
});
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { type FlatViewField } from '@/metadata-store/types/FlatViewField';
|
||||
import { type FlatViewFilter } from '@/metadata-store/types/FlatViewFilter';
|
||||
import { type FlatViewFilterGroup } from '@/metadata-store/types/FlatViewFilterGroup';
|
||||
import { type FlatViewSort } from '@/metadata-store/types/FlatViewSort';
|
||||
import { type RecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewSnapshot';
|
||||
import { type View } from '@/views/types/View';
|
||||
|
||||
export const buildRecordTableWidgetViewSnapshotFromView = (
|
||||
view: View,
|
||||
): RecordTableWidgetViewSnapshot => {
|
||||
const {
|
||||
viewFields,
|
||||
viewFilters,
|
||||
viewFilterGroups,
|
||||
viewSorts,
|
||||
viewGroups: _viewGroups,
|
||||
viewFieldGroups: _viewFieldGroups,
|
||||
...viewProps
|
||||
} = view;
|
||||
|
||||
const flatViewFields: FlatViewField[] = viewFields.map((field) => ({
|
||||
...field,
|
||||
viewId: view.id,
|
||||
}));
|
||||
|
||||
const flatViewFilters: FlatViewFilter[] = viewFilters.map((filter) => ({
|
||||
...filter,
|
||||
viewId: view.id,
|
||||
}));
|
||||
|
||||
const flatViewFilterGroups: FlatViewFilterGroup[] = (
|
||||
viewFilterGroups ?? []
|
||||
).map((group) => ({
|
||||
...group,
|
||||
viewId: view.id,
|
||||
}));
|
||||
|
||||
const flatViewSorts: FlatViewSort[] = viewSorts.map((sort) => ({
|
||||
...sort,
|
||||
viewId: view.id,
|
||||
}));
|
||||
|
||||
return {
|
||||
view: viewProps,
|
||||
viewFields: flatViewFields,
|
||||
viewFilters: flatViewFilters,
|
||||
viewFilterGroups: flatViewFilterGroups,
|
||||
viewSorts: flatViewSorts,
|
||||
};
|
||||
};
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { type RecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewSnapshot';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export const cloneRecordTableWidgetViewSnapshot = (
|
||||
sourceSnapshot: RecordTableWidgetViewSnapshot,
|
||||
): RecordTableWidgetViewSnapshot => {
|
||||
const newViewId = uuidv4();
|
||||
|
||||
const previousToNewFilterGroupId = new Map(
|
||||
sourceSnapshot.viewFilterGroups.map((filterGroup) => [
|
||||
filterGroup.id,
|
||||
uuidv4(),
|
||||
]),
|
||||
);
|
||||
|
||||
return {
|
||||
view: {
|
||||
...sourceSnapshot.view,
|
||||
id: newViewId,
|
||||
},
|
||||
viewFields: sourceSnapshot.viewFields.map((viewField) => ({
|
||||
...viewField,
|
||||
id: uuidv4(),
|
||||
viewId: newViewId,
|
||||
})),
|
||||
viewFilterGroups: sourceSnapshot.viewFilterGroups.map((filterGroup) => ({
|
||||
...filterGroup,
|
||||
id: previousToNewFilterGroupId.get(filterGroup.id) ?? uuidv4(),
|
||||
viewId: newViewId,
|
||||
parentViewFilterGroupId: isDefined(filterGroup.parentViewFilterGroupId)
|
||||
? (previousToNewFilterGroupId.get(
|
||||
filterGroup.parentViewFilterGroupId,
|
||||
) ?? null)
|
||||
: filterGroup.parentViewFilterGroupId,
|
||||
})),
|
||||
viewFilters: sourceSnapshot.viewFilters.map((viewFilter) => ({
|
||||
...viewFilter,
|
||||
id: uuidv4(),
|
||||
viewId: newViewId,
|
||||
viewFilterGroupId: isDefined(viewFilter.viewFilterGroupId)
|
||||
? (previousToNewFilterGroupId.get(viewFilter.viewFilterGroupId) ?? null)
|
||||
: viewFilter.viewFilterGroupId,
|
||||
})),
|
||||
viewSorts: sourceSnapshot.viewSorts.map((viewSort) => ({
|
||||
...viewSort,
|
||||
id: uuidv4(),
|
||||
viewId: newViewId,
|
||||
})),
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user