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:
Charles Bochet
2026-05-14 08:12:00 +02:00
committed by GitHub
parent 6dd1e8a471
commit 04eb913551
37 changed files with 107 additions and 1705 deletions
@@ -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);
});
});
});
@@ -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,
}),
);
});
});
@@ -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);
});
});
@@ -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`,
);
});
});
@@ -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,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,
@@ -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);
};
@@ -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],
};
}),
};
};
@@ -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
);
};