chore(page-layout): remove IS_RECORD_PAGE_LAYOUT_* feature flags (#20556)
## Summary
- Both \`IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED\` and
\`IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED\` are force-enabled on
every existing workspace by the 1.23.0 upgrade command
\`BackfillRecordPageLayoutsCommand\` and seeded enabled for new
workspaces via \`DEFAULT_FEATURE_FLAGS\` +
\`seed-feature-flags.util.ts\`. They are no longer load-bearing.
- Unwrap all \`if (flag) { … }\` conditionals to their enabled branch on
both server and front.
- Delete legacy fallback files that only the disabled branch reached:
\`PageLayoutRelationWidgetsSyncEffect\`,
\`usePageLayoutWithRelationWidgets\`,
\`reInjectDynamicRelationWidgetsFromDraft\`,
\`injectRelationWidgetsIntoLayout\`, \`isDynamicRelationWidget\` (and
their tests).
- Strip the two \`enableFeatureFlags\` calls from the 1.23 upgrade
command — the page-layout backfill data logic itself is kept intact
since old workspaces upgrading from < 1.23 still need it.
- No DB cleanup migration: stale \`featureFlag\` rows are left in place,
matching the precedent set by #20531 and #20460.
Net diff: 37 files, +106 / -1727.
## Test plan
- [x] \`npx nx typecheck twenty-shared twenty-server twenty-front\` —
all pass
- [x] \`npx nx lint:diff-with-main twenty-server twenty-front\` — all
pass
- [x] \`cd packages/twenty-front && npx jest page-layout\` — 1240 tests,
all pass
- [x] \`cd packages/twenty-server && npx jest
workspace-entity-manager.spec\` — pass
- [ ] Manual smoke: open a record page, verify tabs render and \"Edit
Layout\" command-menu action is available
- [ ] Manual smoke: Settings → Data model → object → Layout tab is
visible (and hidden for remote / Dashboard objects)
- [ ] Manual smoke: edit a tab title, save, reload — confirm persistence
This commit is contained in:
-78
@@ -1,7 +1,6 @@
|
||||
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { convertPageLayoutDraftToUpdateInput } from '@/page-layout/utils/convertPageLayoutDraftToUpdateInput';
|
||||
import { DYNAMIC_RELATION_WIDGET_ID_PREFIX } from '@/page-layout/utils/isDynamicRelationWidget';
|
||||
import {
|
||||
PageLayoutTabLayoutMode,
|
||||
PageLayoutType,
|
||||
@@ -350,81 +349,4 @@ describe('convertPageLayoutDraftToUpdateInput', () => {
|
||||
columnSpan: 6,
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldFilterDynamicRelationWidgets', () => {
|
||||
it('should filter out dynamic relation widgets when shouldFilterDynamicRelationWidgets is true', () => {
|
||||
const regularWidget = makeWidget({ id: 'w1' });
|
||||
const dynamicWidget = makeWidget({
|
||||
id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}relation-1`,
|
||||
type: WidgetType.VIEW,
|
||||
});
|
||||
|
||||
const draft = makeDraft([
|
||||
makeTab('tab-1', [regularWidget, dynamicWidget]),
|
||||
]);
|
||||
|
||||
const result = convertPageLayoutDraftToUpdateInput(draft, {
|
||||
shouldFilterDynamicRelationWidgets: true,
|
||||
});
|
||||
|
||||
expect(result.tabs[0].widgets).toHaveLength(1);
|
||||
expect(result.tabs[0].widgets[0].id).toBe('w1');
|
||||
});
|
||||
|
||||
it('should keep all non-dynamic widgets when shouldFilterDynamicRelationWidgets is true and multiple widget types exist', () => {
|
||||
const fieldsWidget = makeWidget({ id: 'w1', type: WidgetType.FIELDS });
|
||||
const timelineWidget = makeWidget({
|
||||
id: 'w2',
|
||||
type: WidgetType.TIMELINE,
|
||||
});
|
||||
const dynamicWidget = makeWidget({
|
||||
id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}rel`,
|
||||
});
|
||||
|
||||
const draft = makeDraft([
|
||||
makeTab('tab-1', [fieldsWidget, dynamicWidget, timelineWidget]),
|
||||
]);
|
||||
|
||||
const result = convertPageLayoutDraftToUpdateInput(draft, {
|
||||
shouldFilterDynamicRelationWidgets: true,
|
||||
});
|
||||
|
||||
expect(result.tabs[0].widgets).toHaveLength(2);
|
||||
expect(result.tabs[0].widgets.map((w) => w.id)).toEqual(['w1', 'w2']);
|
||||
});
|
||||
|
||||
it('should not filter dynamic relation widgets when shouldFilterDynamicRelationWidgets is false', () => {
|
||||
const regularWidget = makeWidget({ id: 'w1' });
|
||||
const dynamicWidget = makeWidget({
|
||||
id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}relation-1`,
|
||||
type: WidgetType.VIEW,
|
||||
});
|
||||
|
||||
const draft = makeDraft([
|
||||
makeTab('tab-1', [regularWidget, dynamicWidget]),
|
||||
]);
|
||||
|
||||
const result = convertPageLayoutDraftToUpdateInput(draft, {
|
||||
shouldFilterDynamicRelationWidgets: false,
|
||||
});
|
||||
|
||||
expect(result.tabs[0].widgets).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should not filter dynamic relation widgets by default', () => {
|
||||
const regularWidget = makeWidget({ id: 'w1' });
|
||||
const dynamicWidget = makeWidget({
|
||||
id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}relation-1`,
|
||||
type: WidgetType.VIEW,
|
||||
});
|
||||
|
||||
const draft = makeDraft([
|
||||
makeTab('tab-1', [regularWidget, dynamicWidget]),
|
||||
]);
|
||||
|
||||
const result = convertPageLayoutDraftToUpdateInput(draft);
|
||||
|
||||
expect(result.tabs[0].widgets).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
-201
@@ -1,201 +0,0 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { injectRelationWidgetsIntoLayout } from '@/page-layout/utils/injectRelationWidgetsIntoLayout';
|
||||
import {
|
||||
FieldDisplayMode,
|
||||
WidgetConfigurationType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const createMockWidget = (
|
||||
id: string,
|
||||
type: WidgetType = WidgetType.FIELDS,
|
||||
): PageLayoutWidget =>
|
||||
({
|
||||
__typename: 'PageLayoutWidget',
|
||||
id,
|
||||
pageLayoutTabId: 'tab-1',
|
||||
title: `Widget ${id}`,
|
||||
type,
|
||||
objectMetadataId: null,
|
||||
gridPosition: {
|
||||
__typename: 'GridPosition',
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 1,
|
||||
columnSpan: 12,
|
||||
},
|
||||
position: {
|
||||
__typename: 'PageLayoutWidgetGridPosition',
|
||||
layoutMode: 'GRID',
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 1,
|
||||
columnSpan: 12,
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
},
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
deletedAt: null,
|
||||
}) as PageLayoutWidget;
|
||||
|
||||
const createMockTab = (
|
||||
id: string,
|
||||
widgets: PageLayoutWidget[],
|
||||
): PageLayoutTab =>
|
||||
({
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id,
|
||||
pageLayoutId: 'page-layout-1',
|
||||
title: `Tab ${id}`,
|
||||
position: 0,
|
||||
widgets,
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
deletedAt: null,
|
||||
}) as PageLayoutTab;
|
||||
|
||||
const createMockLayout = (tabs: PageLayoutTab[]): PageLayout =>
|
||||
({
|
||||
__typename: 'PageLayout',
|
||||
id: 'layout-1',
|
||||
tabs,
|
||||
}) as PageLayout;
|
||||
|
||||
const createMockFieldMetadataItem = (
|
||||
id: string,
|
||||
label: string,
|
||||
): FieldMetadataItem =>
|
||||
({
|
||||
id,
|
||||
label,
|
||||
name: label.toLowerCase(),
|
||||
type: 'RELATION',
|
||||
}) as FieldMetadataItem;
|
||||
|
||||
describe('injectRelationWidgetsIntoLayout', () => {
|
||||
it('should return layout unchanged when relation fields array is empty', () => {
|
||||
const layout = createMockLayout([
|
||||
createMockTab('tab-1', [createMockWidget('w1')]),
|
||||
]);
|
||||
|
||||
const result = injectRelationWidgetsIntoLayout(layout, []);
|
||||
|
||||
expect(result).toBe(layout);
|
||||
});
|
||||
|
||||
it('should return layout unchanged when there are no tabs', () => {
|
||||
const layout = createMockLayout([]);
|
||||
const fields = [createMockFieldMetadataItem('f1', 'Company')];
|
||||
|
||||
const result = injectRelationWidgetsIntoLayout(layout, fields);
|
||||
|
||||
expect(result).toBe(layout);
|
||||
});
|
||||
|
||||
it('should append relation widgets when no FIELDS widget exists', () => {
|
||||
const otherWidget = createMockWidget('w1', WidgetType.TIMELINE);
|
||||
const layout = createMockLayout([createMockTab('tab-1', [otherWidget])]);
|
||||
const fields = [createMockFieldMetadataItem('f1', 'Company')];
|
||||
|
||||
const result = injectRelationWidgetsIntoLayout(layout, fields);
|
||||
|
||||
expect(result.tabs[0].widgets).toHaveLength(2);
|
||||
expect(result.tabs[0].widgets[0].id).toBe('w1');
|
||||
expect(result.tabs[0].widgets[1].id).toContain('dynamic-relation-widget-');
|
||||
expect(result.tabs[0].widgets[1].id).toContain('f1');
|
||||
});
|
||||
|
||||
it('should inject relation widgets after the first FIELDS widget', () => {
|
||||
const fieldsWidget = createMockWidget('fields-1', WidgetType.FIELDS);
|
||||
const timelineWidget = createMockWidget('timeline-1', WidgetType.TIMELINE);
|
||||
const layout = createMockLayout([
|
||||
createMockTab('tab-1', [fieldsWidget, timelineWidget]),
|
||||
]);
|
||||
const fields = [createMockFieldMetadataItem('f1', 'Company')];
|
||||
|
||||
const result = injectRelationWidgetsIntoLayout(layout, fields);
|
||||
|
||||
expect(result.tabs[0].widgets).toHaveLength(3);
|
||||
expect(result.tabs[0].widgets[0].id).toBe('fields-1');
|
||||
expect(result.tabs[0].widgets[1].id).toContain('f1');
|
||||
expect(result.tabs[0].widgets[2].id).toBe('timeline-1');
|
||||
});
|
||||
|
||||
it('should reposition NOTES widget after relation widgets', () => {
|
||||
const fieldsWidget = createMockWidget('fields-1', WidgetType.FIELDS);
|
||||
const notesWidget = createMockWidget('notes-1', WidgetType.NOTES);
|
||||
const timelineWidget = createMockWidget('timeline-1', WidgetType.TIMELINE);
|
||||
const layout = createMockLayout([
|
||||
createMockTab('tab-1', [fieldsWidget, notesWidget, timelineWidget]),
|
||||
]);
|
||||
const fields = [createMockFieldMetadataItem('f1', 'Company')];
|
||||
|
||||
const result = injectRelationWidgetsIntoLayout(layout, fields);
|
||||
|
||||
const widgetIds = result.tabs[0].widgets.map((w) => w.id);
|
||||
|
||||
expect(widgetIds[0]).toBe('fields-1');
|
||||
expect(widgetIds[1]).toContain('f1');
|
||||
expect(widgetIds[2]).toBe('notes-1');
|
||||
expect(widgetIds[3]).toBe('timeline-1');
|
||||
});
|
||||
|
||||
it('should only modify the first tab', () => {
|
||||
const layout = createMockLayout([
|
||||
createMockTab('tab-1', [createMockWidget('fields-1', WidgetType.FIELDS)]),
|
||||
createMockTab('tab-2', [createMockWidget('fields-2', WidgetType.FIELDS)]),
|
||||
]);
|
||||
const fields = [createMockFieldMetadataItem('f1', 'Company')];
|
||||
|
||||
const result = injectRelationWidgetsIntoLayout(layout, fields);
|
||||
|
||||
expect(result.tabs[0].widgets).toHaveLength(2);
|
||||
expect(result.tabs[1].widgets).toHaveLength(1);
|
||||
expect(result.tabs[1].widgets[0].id).toBe('fields-2');
|
||||
});
|
||||
|
||||
it('should inject multiple relation widgets in order', () => {
|
||||
const fieldsWidget = createMockWidget('fields-1', WidgetType.FIELDS);
|
||||
const layout = createMockLayout([createMockTab('tab-1', [fieldsWidget])]);
|
||||
const fields = [
|
||||
createMockFieldMetadataItem('f1', 'Company'),
|
||||
createMockFieldMetadataItem('f2', 'Person'),
|
||||
];
|
||||
|
||||
const result = injectRelationWidgetsIntoLayout(layout, fields);
|
||||
|
||||
expect(result.tabs[0].widgets).toHaveLength(3);
|
||||
expect(result.tabs[0].widgets[0].id).toBe('fields-1');
|
||||
expect(result.tabs[0].widgets[1].id).toContain('f1');
|
||||
expect(result.tabs[0].widgets[2].id).toContain('f2');
|
||||
});
|
||||
|
||||
it('should set correct properties on injected relation widgets', () => {
|
||||
const layout = createMockLayout([
|
||||
createMockTab('tab-1', [createMockWidget('fields-1', WidgetType.FIELDS)]),
|
||||
]);
|
||||
const fields = [createMockFieldMetadataItem('f1', 'Company')];
|
||||
|
||||
const result = injectRelationWidgetsIntoLayout(layout, fields);
|
||||
|
||||
const injectedWidget = result.tabs[0].widgets[1];
|
||||
|
||||
expect(injectedWidget.type).toBe(WidgetType.FIELD);
|
||||
expect(injectedWidget.title).toBe('Company');
|
||||
expect(injectedWidget.pageLayoutTabId).toBe('tab-1');
|
||||
expect(injectedWidget.configuration).toEqual(
|
||||
expect.objectContaining({
|
||||
fieldMetadataId: 'f1',
|
||||
fieldDisplayMode: FieldDisplayMode.CARD,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import {
|
||||
DYNAMIC_RELATION_WIDGET_ID_PREFIX,
|
||||
isDynamicRelationWidget,
|
||||
} from '@/page-layout/utils/isDynamicRelationWidget';
|
||||
|
||||
const makeWidget = (id: string): PageLayoutWidget =>
|
||||
({
|
||||
id,
|
||||
title: 'Widget',
|
||||
type: 'FIELDS',
|
||||
pageLayoutTabId: 'tab-1',
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 1, columnSpan: 1 },
|
||||
configuration: {},
|
||||
}) as PageLayoutWidget;
|
||||
|
||||
describe('isDynamicRelationWidget', () => {
|
||||
it('should return true for a widget with the dynamic relation prefix', () => {
|
||||
const widget = makeWidget(
|
||||
`${DYNAMIC_RELATION_WIDGET_ID_PREFIX}some-relation`,
|
||||
);
|
||||
|
||||
expect(isDynamicRelationWidget(widget)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for a widget without the dynamic relation prefix', () => {
|
||||
const widget = makeWidget('regular-widget-id');
|
||||
|
||||
expect(isDynamicRelationWidget(widget)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for a widget with a similar but different prefix', () => {
|
||||
const widget = makeWidget('dynamic-relation-other-widget-123');
|
||||
|
||||
expect(isDynamicRelationWidget(widget)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for the prefix alone as id', () => {
|
||||
const widget = makeWidget(DYNAMIC_RELATION_WIDGET_ID_PREFIX);
|
||||
|
||||
expect(isDynamicRelationWidget(widget)).toBe(true);
|
||||
});
|
||||
});
|
||||
-194
@@ -1,194 +0,0 @@
|
||||
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
|
||||
import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { DYNAMIC_RELATION_WIDGET_ID_PREFIX } from '@/page-layout/utils/isDynamicRelationWidget';
|
||||
import { reInjectDynamicRelationWidgetsFromDraft } from '@/page-layout/utils/reInjectDynamicRelationWidgetsFromDraft';
|
||||
import { WidgetType } from '~/generated-metadata/graphql';
|
||||
|
||||
const makeWidget = (
|
||||
overrides: Partial<PageLayoutWidget> & { id: string },
|
||||
): PageLayoutWidget =>
|
||||
({
|
||||
title: 'Widget',
|
||||
type: WidgetType.FIELDS,
|
||||
pageLayoutTabId: 'tab-1',
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 1, columnSpan: 1 },
|
||||
configuration: {},
|
||||
...overrides,
|
||||
}) as PageLayoutWidget;
|
||||
|
||||
const makeDynamicRelationWidget = (suffix: string): PageLayoutWidget =>
|
||||
makeWidget({
|
||||
id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}${suffix}`,
|
||||
type: WidgetType.VIEW,
|
||||
title: `Dynamic Relation ${suffix}`,
|
||||
});
|
||||
|
||||
const makePersistedLayout = (tabs: PageLayout['tabs']): PageLayout =>
|
||||
({
|
||||
id: 'layout-1',
|
||||
name: 'Layout',
|
||||
type: 'RECORD_PAGE',
|
||||
createdAt: '2025-01-01',
|
||||
updatedAt: '2025-01-01',
|
||||
tabs,
|
||||
}) as PageLayout;
|
||||
|
||||
const makeDraftLayout = (tabs: DraftPageLayout['tabs']): DraftPageLayout =>
|
||||
({
|
||||
id: 'layout-1',
|
||||
name: 'Layout',
|
||||
type: 'RECORD_PAGE',
|
||||
tabs,
|
||||
}) as DraftPageLayout;
|
||||
|
||||
const makeTab = (
|
||||
id: string,
|
||||
widgets: PageLayoutWidget[],
|
||||
): PageLayout['tabs'][number] =>
|
||||
({
|
||||
id,
|
||||
title: 'Tab',
|
||||
position: 0,
|
||||
pageLayoutId: 'layout-1',
|
||||
applicationId: 'app-1',
|
||||
createdAt: '2025-01-01',
|
||||
updatedAt: '2025-01-01',
|
||||
widgets,
|
||||
}) as PageLayout['tabs'][number];
|
||||
|
||||
describe('reInjectDynamicRelationWidgetsFromDraft', () => {
|
||||
it('should return persisted layout unchanged when draft has no dynamic widgets', () => {
|
||||
const persistedLayout = makePersistedLayout([
|
||||
makeTab('tab-1', [makeWidget({ id: 'w1' })]),
|
||||
]);
|
||||
const draft = makeDraftLayout([
|
||||
makeTab('tab-1', [makeWidget({ id: 'w1' })]),
|
||||
]);
|
||||
|
||||
const result = reInjectDynamicRelationWidgetsFromDraft(
|
||||
persistedLayout,
|
||||
draft,
|
||||
);
|
||||
|
||||
expect(result).toBe(persistedLayout);
|
||||
});
|
||||
|
||||
it('should inject dynamic widgets after the first FIELDS widget', () => {
|
||||
const fieldsWidget = makeWidget({
|
||||
id: 'fields-1',
|
||||
type: WidgetType.FIELDS,
|
||||
});
|
||||
const timelineWidget = makeWidget({
|
||||
id: 'timeline-1',
|
||||
type: WidgetType.TIMELINE,
|
||||
});
|
||||
const dynamicWidget = makeDynamicRelationWidget('relation-1');
|
||||
|
||||
const persistedLayout = makePersistedLayout([
|
||||
makeTab('tab-1', [fieldsWidget, timelineWidget]),
|
||||
]);
|
||||
const draft = makeDraftLayout([
|
||||
makeTab('tab-1', [fieldsWidget, dynamicWidget, timelineWidget]),
|
||||
]);
|
||||
|
||||
const result = reInjectDynamicRelationWidgetsFromDraft(
|
||||
persistedLayout,
|
||||
draft,
|
||||
);
|
||||
|
||||
expect(result.tabs[0].widgets).toHaveLength(3);
|
||||
expect(result.tabs[0].widgets[0].id).toBe('fields-1');
|
||||
expect(result.tabs[0].widgets[1].id).toBe(
|
||||
`${DYNAMIC_RELATION_WIDGET_ID_PREFIX}relation-1`,
|
||||
);
|
||||
expect(result.tabs[0].widgets[2].id).toBe('timeline-1');
|
||||
});
|
||||
|
||||
it('should append dynamic widgets if no FIELDS widget exists in the tab', () => {
|
||||
const timelineWidget = makeWidget({
|
||||
id: 'timeline-1',
|
||||
type: WidgetType.TIMELINE,
|
||||
});
|
||||
const dynamicWidget = makeDynamicRelationWidget('relation-1');
|
||||
|
||||
const persistedLayout = makePersistedLayout([
|
||||
makeTab('tab-1', [timelineWidget]),
|
||||
]);
|
||||
const draft = makeDraftLayout([
|
||||
makeTab('tab-1', [timelineWidget, dynamicWidget]),
|
||||
]);
|
||||
|
||||
const result = reInjectDynamicRelationWidgetsFromDraft(
|
||||
persistedLayout,
|
||||
draft,
|
||||
);
|
||||
|
||||
expect(result.tabs[0].widgets).toHaveLength(2);
|
||||
expect(result.tabs[0].widgets[0].id).toBe('timeline-1');
|
||||
expect(result.tabs[0].widgets[1].id).toBe(
|
||||
`${DYNAMIC_RELATION_WIDGET_ID_PREFIX}relation-1`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle multiple tabs independently', () => {
|
||||
const fieldsWidget1 = makeWidget({
|
||||
id: 'fields-1',
|
||||
type: WidgetType.FIELDS,
|
||||
});
|
||||
const fieldsWidget2 = makeWidget({
|
||||
id: 'fields-2',
|
||||
type: WidgetType.FIELDS,
|
||||
});
|
||||
const dynamicWidget = makeDynamicRelationWidget('rel-1');
|
||||
|
||||
const persistedLayout = makePersistedLayout([
|
||||
makeTab('tab-1', [fieldsWidget1]),
|
||||
makeTab('tab-2', [fieldsWidget2]),
|
||||
]);
|
||||
const draft = makeDraftLayout([
|
||||
makeTab('tab-1', [fieldsWidget1, dynamicWidget]),
|
||||
makeTab('tab-2', [fieldsWidget2]),
|
||||
]);
|
||||
|
||||
const result = reInjectDynamicRelationWidgetsFromDraft(
|
||||
persistedLayout,
|
||||
draft,
|
||||
);
|
||||
|
||||
// Tab 1 should have dynamic widget injected
|
||||
expect(result.tabs[0].widgets).toHaveLength(2);
|
||||
// Tab 2 should remain unchanged
|
||||
expect(result.tabs[1].widgets).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should inject multiple dynamic widgets', () => {
|
||||
const fieldsWidget = makeWidget({
|
||||
id: 'fields-1',
|
||||
type: WidgetType.FIELDS,
|
||||
});
|
||||
const dynamicWidget1 = makeDynamicRelationWidget('rel-1');
|
||||
const dynamicWidget2 = makeDynamicRelationWidget('rel-2');
|
||||
|
||||
const persistedLayout = makePersistedLayout([
|
||||
makeTab('tab-1', [fieldsWidget]),
|
||||
]);
|
||||
const draft = makeDraftLayout([
|
||||
makeTab('tab-1', [fieldsWidget, dynamicWidget1, dynamicWidget2]),
|
||||
]);
|
||||
|
||||
const result = reInjectDynamicRelationWidgetsFromDraft(
|
||||
persistedLayout,
|
||||
draft,
|
||||
);
|
||||
|
||||
expect(result.tabs[0].widgets).toHaveLength(3);
|
||||
expect(result.tabs[0].widgets[0].id).toBe('fields-1');
|
||||
expect(result.tabs[0].widgets[1].id).toBe(
|
||||
`${DYNAMIC_RELATION_WIDGET_ID_PREFIX}rel-1`,
|
||||
);
|
||||
expect(result.tabs[0].widgets[2].id).toBe(
|
||||
`${DYNAMIC_RELATION_WIDGET_ID_PREFIX}rel-2`,
|
||||
);
|
||||
});
|
||||
});
|
||||
+1
-49
@@ -14,61 +14,13 @@ describe('shouldEnableTabEditingFeatures', () => {
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for RECORD_PAGE layout type without flag', () => {
|
||||
it('should return true for RECORD_PAGE layout type', () => {
|
||||
const result = shouldEnableTabEditingFeatures(PageLayoutType.RECORD_PAGE);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for RECORD_PAGE layout type with flag enabled', () => {
|
||||
const result = shouldEnableTabEditingFeatures(
|
||||
PageLayoutType.RECORD_PAGE,
|
||||
true,
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for RECORD_PAGE layout type with flag disabled', () => {
|
||||
const result = shouldEnableTabEditingFeatures(
|
||||
PageLayoutType.RECORD_PAGE,
|
||||
false,
|
||||
);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for RECORD_INDEX layout type', () => {
|
||||
const result = shouldEnableTabEditingFeatures(PageLayoutType.RECORD_INDEX);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for RECORD_INDEX layout type even with flag', () => {
|
||||
const result = shouldEnableTabEditingFeatures(
|
||||
PageLayoutType.RECORD_INDEX,
|
||||
true,
|
||||
);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
describe('behavior validation', () => {
|
||||
it('should enable tab editing features only for dashboards, standalone pages, and record pages with flag', () => {
|
||||
expect(shouldEnableTabEditingFeatures(PageLayoutType.DASHBOARD)).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
expect(
|
||||
shouldEnableTabEditingFeatures(PageLayoutType.STANDALONE_PAGE),
|
||||
).toBe(true);
|
||||
|
||||
expect(shouldEnableTabEditingFeatures(PageLayoutType.RECORD_PAGE)).toBe(
|
||||
false,
|
||||
);
|
||||
|
||||
expect(
|
||||
shouldEnableTabEditingFeatures(PageLayoutType.RECORD_PAGE, true),
|
||||
).toBe(true);
|
||||
|
||||
expect(shouldEnableTabEditingFeatures(PageLayoutType.RECORD_INDEX)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+1
-9
@@ -1,6 +1,5 @@
|
||||
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { isDynamicRelationWidget } from '@/page-layout/utils/isDynamicRelationWidget';
|
||||
import {
|
||||
PageLayoutTabLayoutMode,
|
||||
type UpdatePageLayoutWithTabsInput,
|
||||
@@ -40,10 +39,7 @@ const buildWidgetPosition = (
|
||||
|
||||
export const convertPageLayoutDraftToUpdateInput = (
|
||||
pageLayoutDraft: DraftPageLayout,
|
||||
options?: { shouldFilterDynamicRelationWidgets?: boolean },
|
||||
): UpdatePageLayoutWithTabsInput => {
|
||||
const shouldFilter = options?.shouldFilterDynamicRelationWidgets ?? false;
|
||||
|
||||
return {
|
||||
name: pageLayoutDraft.name,
|
||||
type: pageLayoutDraft.type,
|
||||
@@ -51,17 +47,13 @@ export const convertPageLayoutDraftToUpdateInput = (
|
||||
tabs: pageLayoutDraft.tabs
|
||||
.filter((tab) => tab.isActive)
|
||||
.map((tab) => {
|
||||
const widgets = shouldFilter
|
||||
? tab.widgets.filter((widget) => !isDynamicRelationWidget(widget))
|
||||
: tab.widgets;
|
||||
|
||||
return {
|
||||
id: tab.id,
|
||||
title: tab.title,
|
||||
position: tab.position,
|
||||
icon: tab.icon ?? null,
|
||||
layoutMode: tab.layoutMode,
|
||||
widgets: widgets.map((widget, widgetIndex) => ({
|
||||
widgets: tab.widgets.map((widget, widgetIndex) => ({
|
||||
id: widget.id,
|
||||
pageLayoutTabId: widget.pageLayoutTabId,
|
||||
title: widget.title,
|
||||
|
||||
-131
@@ -1,131 +0,0 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { DYNAMIC_RELATION_WIDGET_ID_PREFIX } from '@/page-layout/utils/isDynamicRelationWidget';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
FieldDisplayMode,
|
||||
PageLayoutTabLayoutMode,
|
||||
WidgetConfigurationType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const getRelationFieldWidgetToInsert = (
|
||||
field: FieldMetadataItem,
|
||||
tabId: string,
|
||||
): PageLayoutWidget => ({
|
||||
__typename: 'PageLayoutWidget' as const,
|
||||
id: `${DYNAMIC_RELATION_WIDGET_ID_PREFIX}${field.id}-${field.label}`,
|
||||
applicationId: '',
|
||||
pageLayoutTabId: tabId,
|
||||
title: field.label,
|
||||
isActive: true,
|
||||
type: WidgetType.FIELD,
|
||||
objectMetadataId: null,
|
||||
gridPosition: {
|
||||
__typename: 'GridPosition' as const,
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 1,
|
||||
columnSpan: 12,
|
||||
},
|
||||
position: {
|
||||
__typename: 'PageLayoutWidgetGridPosition' as const,
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 1,
|
||||
columnSpan: 12,
|
||||
},
|
||||
configuration: {
|
||||
__typename: 'FieldConfiguration' as const,
|
||||
configurationType: WidgetConfigurationType.FIELD,
|
||||
fieldMetadataId: field.id,
|
||||
fieldDisplayMode: FieldDisplayMode.CARD,
|
||||
},
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
deletedAt: null,
|
||||
});
|
||||
|
||||
const getRelationFieldWidgetsToInsert = (
|
||||
relationFields: FieldMetadataItem[],
|
||||
tabId: string,
|
||||
): PageLayoutWidget[] => {
|
||||
return relationFields.map((field) =>
|
||||
getRelationFieldWidgetToInsert(field, tabId),
|
||||
);
|
||||
};
|
||||
|
||||
export const injectRelationWidgetsIntoLayout = (
|
||||
layout: PageLayout,
|
||||
boxedRelationFieldMetadataItems: FieldMetadataItem[],
|
||||
): PageLayout => {
|
||||
if (boxedRelationFieldMetadataItems.length === 0) {
|
||||
return layout;
|
||||
}
|
||||
|
||||
const firstTab = layout.tabs[0];
|
||||
if (!isDefined(firstTab)) {
|
||||
return layout;
|
||||
}
|
||||
|
||||
const relationWidgets = getRelationFieldWidgetsToInsert(
|
||||
boxedRelationFieldMetadataItems,
|
||||
firstTab.id,
|
||||
);
|
||||
|
||||
return {
|
||||
...layout,
|
||||
tabs: layout.tabs.map((tab) => {
|
||||
if (tab.id === firstTab.id) {
|
||||
const firstFieldsWidgetIndex = tab.widgets.findIndex(
|
||||
(widget) => widget.type === WidgetType.FIELDS,
|
||||
);
|
||||
|
||||
if (firstFieldsWidgetIndex === -1) {
|
||||
return {
|
||||
...tab,
|
||||
widgets: [...tab.widgets, ...relationWidgets],
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: This note widget repositioning logic is temporary and will be deleted soon.
|
||||
// We need this to ensure the note editor is displayed before record relations,
|
||||
// matching the behavior of the old show page.
|
||||
const noteWidgetIndex = tab.widgets.findIndex(
|
||||
(widget) => widget.type === WidgetType.NOTES,
|
||||
);
|
||||
|
||||
const widgetsBeforeRelation = tab.widgets.slice(
|
||||
0,
|
||||
firstFieldsWidgetIndex + 1,
|
||||
);
|
||||
const widgetsAfterRelation =
|
||||
noteWidgetIndex === -1
|
||||
? tab.widgets.slice(firstFieldsWidgetIndex + 1)
|
||||
: [
|
||||
...tab.widgets.slice(
|
||||
firstFieldsWidgetIndex + 1,
|
||||
noteWidgetIndex,
|
||||
),
|
||||
...tab.widgets.slice(noteWidgetIndex + 1),
|
||||
];
|
||||
|
||||
const noteWidget =
|
||||
noteWidgetIndex !== -1 ? [tab.widgets[noteWidgetIndex]] : [];
|
||||
|
||||
return {
|
||||
...tab,
|
||||
widgets: [
|
||||
...widgetsBeforeRelation,
|
||||
...relationWidgets,
|
||||
...noteWidget,
|
||||
...widgetsAfterRelation,
|
||||
],
|
||||
};
|
||||
}
|
||||
return tab;
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
|
||||
export const DYNAMIC_RELATION_WIDGET_ID_PREFIX = 'dynamic-relation-widget-';
|
||||
|
||||
export const isDynamicRelationWidget = (widget: PageLayoutWidget): boolean => {
|
||||
return widget.id.startsWith(DYNAMIC_RELATION_WIDGET_ID_PREFIX);
|
||||
};
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
|
||||
import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import { isDynamicRelationWidget } from '@/page-layout/utils/isDynamicRelationWidget';
|
||||
import { WidgetType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const reInjectDynamicRelationWidgetsFromDraft = (
|
||||
persistedLayout: PageLayout,
|
||||
previousDraft: DraftPageLayout,
|
||||
): PageLayout => {
|
||||
const dynamicWidgetsByTabId = new Map(
|
||||
previousDraft.tabs.map((tab) => [
|
||||
tab.id,
|
||||
tab.widgets.filter(isDynamicRelationWidget),
|
||||
]),
|
||||
);
|
||||
|
||||
const hasDynamicWidgets = [...dynamicWidgetsByTabId.values()].some(
|
||||
(widgets) => widgets.length > 0,
|
||||
);
|
||||
|
||||
if (!hasDynamicWidgets) {
|
||||
return persistedLayout;
|
||||
}
|
||||
|
||||
return {
|
||||
...persistedLayout,
|
||||
tabs: persistedLayout.tabs.map((tab) => {
|
||||
const relationWidgets = dynamicWidgetsByTabId.get(tab.id);
|
||||
|
||||
if (!relationWidgets || relationWidgets.length === 0) {
|
||||
return tab;
|
||||
}
|
||||
|
||||
const firstFieldsWidgetIndex = tab.widgets.findIndex(
|
||||
(widget) => widget.type === WidgetType.FIELDS,
|
||||
);
|
||||
|
||||
if (firstFieldsWidgetIndex === -1) {
|
||||
return {
|
||||
...tab,
|
||||
widgets: [...tab.widgets, ...relationWidgets],
|
||||
};
|
||||
}
|
||||
|
||||
const widgetsBefore = tab.widgets.slice(0, firstFieldsWidgetIndex + 1);
|
||||
const widgetsAfter = tab.widgets.slice(firstFieldsWidgetIndex + 1);
|
||||
|
||||
return {
|
||||
...tab,
|
||||
widgets: [...widgetsBefore, ...relationWidgets, ...widgetsAfter],
|
||||
};
|
||||
}),
|
||||
};
|
||||
};
|
||||
+4
-15
@@ -2,21 +2,10 @@ import { PageLayoutType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const shouldEnableTabEditingFeatures = (
|
||||
pageLayoutType: PageLayoutType,
|
||||
isRecordPageGlobalEditionEnabled?: boolean,
|
||||
): boolean => {
|
||||
if (
|
||||
return (
|
||||
pageLayoutType === PageLayoutType.DASHBOARD ||
|
||||
pageLayoutType === PageLayoutType.STANDALONE_PAGE
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
pageLayoutType === PageLayoutType.RECORD_PAGE &&
|
||||
isRecordPageGlobalEditionEnabled
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
pageLayoutType === PageLayoutType.STANDALONE_PAGE ||
|
||||
pageLayoutType === PageLayoutType.RECORD_PAGE
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user