Introduce standalone page (#19675)

Add support for standalone pages: a new `PageLayout` type
(`STANDALONE_PAGE`) that can be rendered independently at
`/page/:pageLayoutId`, not tied to any record or object context.

- New `STANDALONE_PAGE` page layout type
- New `PAGE_LAYOUT` navigation menu item type: adds a `pageLayoutId`
foreign key to `NavigationMenuItemEntity`, allowing sidebar items to
link directly to standalone pages
- New `GLOBAL_OBJECT_CONTEXT` command menu availability type: separates
object-context-dependent commands (Create Record, Import, Export, See
Deleted, Create View, Hide Deleted) from truly global ones, so
standalone pages only show relevant commands
- Frontend routing & rendering: adds a `/page/:pageLayoutId` route with
its own page component, header, and command menu
- Widget rendering refactor
- Instance commands: two fast 1.22 migrations: `pageLayoutId` column +
`STANDALONE_PAGE` enum, and `GLOBAL_OBJECT_CONTEXT` availability type
enum
- Workspace command: backfills existing command menu items from `GLOBAL`
to `GLOBAL_OBJECT_CONTEXT` where appropriate
- Dev seeds: adds a sample "Star History" standalone page with an iframe
widget for local development
This commit is contained in:
Raphaël Bosi
2026-04-14 17:52:45 +02:00
committed by GitHub
parent 43249d80e8
commit a88d1f4442
122 changed files with 2250 additions and 836 deletions
@@ -0,0 +1,3 @@
export const NAVIGATION_MENU_ITEM_SEEDS = {
DOCUMENTATION_PAGE: 'DOCUMENTATION_PAGE_NAV_ITEM',
};
@@ -2,4 +2,5 @@ export const PAGE_LAYOUT_SEEDS = {
SALES_DASHBOARD: 'SALES_DASHBOARD',
CUSTOMER_DASHBOARD: 'CUSTOMER_DASHBOARD',
TEAM_DASHBOARD: 'TEAM_DASHBOARD',
DOCUMENTATION_STANDALONE_PAGE: 'DOCUMENTATION_STANDALONE_PAGE',
};
@@ -5,4 +5,5 @@ export const PAGE_LAYOUT_TAB_SEEDS = {
CUSTOMER_ANALYTICS: 'CUSTOMER_ANALYTICS_TAB',
TEAM_OVERVIEW: 'TEAM_OVERVIEW_TAB',
TEAM_METRICS: 'TEAM_METRICS_TAB',
DOCUMENTATION: 'DOCUMENTATION',
};
@@ -21,4 +21,5 @@ export const PAGE_LAYOUT_WIDGET_SEEDS = {
TEAM_OPEN_TASKS: 'TEAM_OPEN_TASKS_WIDGET',
FRONT_COMPONENT: 'FRONT_COMPONENT_WIDGET',
DOCUMENTATION_IFRAME: 'DOCUMENTATION_IFRAME_WIDGET',
};
@@ -0,0 +1,56 @@
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type';
import { NavigationMenuItemType } from 'src/engine/metadata-modules/navigation-menu-item/enums/navigation-menu-item-type.enum';
import { NAVIGATION_MENU_ITEM_SEEDS } from 'src/engine/workspace-manager/dev-seeder/core/constants/navigation-menu-item-seeds.constant';
import { PAGE_LAYOUT_SEEDS } from 'src/engine/workspace-manager/dev-seeder/core/constants/page-layout-seeds.constant';
import { generateSeedId } from 'src/engine/workspace-manager/dev-seeder/core/utils/generate-seed-id.util';
export const getNavigationMenuItemFlatEntitySeeds = ({
workspaceId,
flatApplication,
}: {
workspaceId: string;
flatApplication: FlatApplication;
}): FlatNavigationMenuItem[] => {
const now = new Date().toISOString();
return [
{
id: generateSeedId(
workspaceId,
NAVIGATION_MENU_ITEM_SEEDS.DOCUMENTATION_PAGE,
),
universalIdentifier: generateSeedId(
workspaceId,
NAVIGATION_MENU_ITEM_SEEDS.DOCUMENTATION_PAGE,
),
applicationId: flatApplication.id,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
workspaceId,
type: NavigationMenuItemType.PAGE_LAYOUT,
name: 'Star History',
icon: 'IconStar',
color: 'yellow',
position: 9999,
link: null,
userWorkspaceId: null,
targetRecordId: null,
targetObjectMetadataId: null,
targetObjectMetadataUniversalIdentifier: null,
viewId: null,
viewUniversalIdentifier: null,
folderId: null,
folderUniversalIdentifier: null,
pageLayoutId: generateSeedId(
workspaceId,
PAGE_LAYOUT_SEEDS.DOCUMENTATION_STANDALONE_PAGE,
),
pageLayoutUniversalIdentifier: generateSeedId(
workspaceId,
PAGE_LAYOUT_SEEDS.DOCUMENTATION_STANDALONE_PAGE,
),
createdAt: now,
updatedAt: now,
},
];
};
@@ -1,48 +1,105 @@
import { v4 } from 'uuid';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
import { PAGE_LAYOUT_SEEDS } from 'src/engine/workspace-manager/dev-seeder/core/constants/page-layout-seeds.constant';
import { generateSeedId } from 'src/engine/workspace-manager/dev-seeder/core/utils/generate-seed-id.util';
type PageLayoutSeed = {
id: string;
name: string;
type: PageLayoutType;
objectMetadataId: string | null;
export const getPageLayoutFlatEntitySeeds = ({
workspaceId,
flatApplication,
}: {
workspaceId: string;
universalIdentifier: string;
applicationId: string;
};
flatApplication: FlatApplication;
}): FlatPageLayout[] => {
const now = new Date().toISOString();
export const getPageLayoutDataSeeds = (
workspaceId: string,
applicationId: string,
): PageLayoutSeed[] => [
{
id: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.SALES_DASHBOARD),
name: 'Sales Dashboard Layout',
type: PageLayoutType.DASHBOARD,
objectMetadataId: null,
workspaceId,
universalIdentifier: v4(),
applicationId,
},
{
id: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.CUSTOMER_DASHBOARD),
name: 'Customer Dashboard Layout',
type: PageLayoutType.DASHBOARD,
objectMetadataId: null,
workspaceId,
universalIdentifier: v4(),
applicationId,
},
{
id: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.TEAM_DASHBOARD),
name: 'Team Dashboard Layout',
type: PageLayoutType.DASHBOARD,
objectMetadataId: null,
workspaceId,
universalIdentifier: v4(),
applicationId,
},
];
return [
{
id: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.SALES_DASHBOARD),
universalIdentifier: generateSeedId(
workspaceId,
PAGE_LAYOUT_SEEDS.SALES_DASHBOARD,
),
applicationId: flatApplication.id,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
workspaceId,
name: 'Sales Dashboard Layout',
type: PageLayoutType.DASHBOARD,
objectMetadataId: null,
objectMetadataUniversalIdentifier: null,
tabIds: [],
tabUniversalIdentifiers: [],
defaultTabToFocusOnMobileAndSidePanelId: null,
defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier: null,
createdAt: now,
updatedAt: now,
deletedAt: null,
},
{
id: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.CUSTOMER_DASHBOARD),
universalIdentifier: generateSeedId(
workspaceId,
PAGE_LAYOUT_SEEDS.CUSTOMER_DASHBOARD,
),
applicationId: flatApplication.id,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
workspaceId,
name: 'Customer Dashboard Layout',
type: PageLayoutType.DASHBOARD,
objectMetadataId: null,
objectMetadataUniversalIdentifier: null,
tabIds: [],
tabUniversalIdentifiers: [],
defaultTabToFocusOnMobileAndSidePanelId: null,
defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier: null,
createdAt: now,
updatedAt: now,
deletedAt: null,
},
{
id: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.TEAM_DASHBOARD),
universalIdentifier: generateSeedId(
workspaceId,
PAGE_LAYOUT_SEEDS.TEAM_DASHBOARD,
),
applicationId: flatApplication.id,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
workspaceId,
name: 'Team Dashboard Layout',
type: PageLayoutType.DASHBOARD,
objectMetadataId: null,
objectMetadataUniversalIdentifier: null,
tabIds: [],
tabUniversalIdentifiers: [],
defaultTabToFocusOnMobileAndSidePanelId: null,
defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier: null,
createdAt: now,
updatedAt: now,
deletedAt: null,
},
{
id: generateSeedId(
workspaceId,
PAGE_LAYOUT_SEEDS.DOCUMENTATION_STANDALONE_PAGE,
),
universalIdentifier: generateSeedId(
workspaceId,
PAGE_LAYOUT_SEEDS.DOCUMENTATION_STANDALONE_PAGE,
),
applicationId: flatApplication.id,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
workspaceId,
name: 'Documentation',
type: PageLayoutType.STANDALONE_PAGE,
objectMetadataId: null,
objectMetadataUniversalIdentifier: null,
tabIds: [],
tabUniversalIdentifiers: [],
defaultTabToFocusOnMobileAndSidePanelId: null,
defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier: null,
createdAt: now,
updatedAt: now,
deletedAt: null,
},
];
};
@@ -1,98 +1,88 @@
import { v4 } from 'uuid';
import { PageLayoutTabLayoutMode } from 'twenty-shared/types';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
import { PAGE_LAYOUT_SEEDS } from 'src/engine/workspace-manager/dev-seeder/core/constants/page-layout-seeds.constant';
import { PAGE_LAYOUT_TAB_SEEDS } from 'src/engine/workspace-manager/dev-seeder/core/constants/page-layout-tab-seeds.constant';
import { generateSeedId } from 'src/engine/workspace-manager/dev-seeder/core/utils/generate-seed-id.util';
type PageLayoutTabSeed = {
id: string;
title: string;
position: number;
pageLayoutId: string;
workspaceId: string;
} & Pick<
FlatPageLayoutTab,
'applicationId' | 'universalIdentifier' | 'overrides'
>;
export const getPageLayoutTabDataSeeds = ({
applicationId,
export const getPageLayoutTabFlatEntitySeeds = ({
workspaceId,
flatApplication,
}: {
workspaceId: string;
applicationId: string;
}): PageLayoutTabSeed[] => [
{
id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.SALES_OVERVIEW),
title: 'Overview',
position: 0,
pageLayoutId: generateSeedId(
workspaceId,
flatApplication: FlatApplication;
}): FlatPageLayoutTab[] => {
const now = new Date().toISOString();
const buildTab = (
tabSeed: string,
title: string,
position: number,
pageLayoutSeed: string,
): FlatPageLayoutTab => ({
id: generateSeedId(workspaceId, tabSeed),
universalIdentifier: generateSeedId(workspaceId, tabSeed),
applicationId: flatApplication.id,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
workspaceId,
title,
position,
pageLayoutId: generateSeedId(workspaceId, pageLayoutSeed),
pageLayoutUniversalIdentifier: generateSeedId(workspaceId, pageLayoutSeed),
widgetIds: [],
widgetUniversalIdentifiers: [],
isActive: true,
icon: null,
layoutMode: PageLayoutTabLayoutMode.GRID,
overrides: null,
createdAt: now,
updatedAt: now,
deletedAt: null,
});
return [
buildTab(
PAGE_LAYOUT_TAB_SEEDS.SALES_OVERVIEW,
'Overview',
0,
PAGE_LAYOUT_SEEDS.SALES_DASHBOARD,
),
workspaceId,
universalIdentifier: v4(),
applicationId,
overrides: null,
},
{
id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.SALES_DETAILS),
title: 'Details',
position: 1,
pageLayoutId: generateSeedId(
workspaceId,
buildTab(
PAGE_LAYOUT_TAB_SEEDS.SALES_DETAILS,
'Details',
1,
PAGE_LAYOUT_SEEDS.SALES_DASHBOARD,
),
workspaceId,
universalIdentifier: v4(),
applicationId,
overrides: null,
},
{
id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_OVERVIEW),
title: 'Overview',
position: 0,
pageLayoutId: generateSeedId(
workspaceId,
buildTab(
PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_OVERVIEW,
'Overview',
0,
PAGE_LAYOUT_SEEDS.CUSTOMER_DASHBOARD,
),
workspaceId,
universalIdentifier: v4(),
applicationId,
overrides: null,
},
{
id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_ANALYTICS),
title: 'Analytics',
position: 1,
pageLayoutId: generateSeedId(
workspaceId,
buildTab(
PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_ANALYTICS,
'Analytics',
1,
PAGE_LAYOUT_SEEDS.CUSTOMER_DASHBOARD,
),
workspaceId,
universalIdentifier: v4(),
applicationId,
overrides: null,
},
{
id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.TEAM_OVERVIEW),
title: 'Team & People',
position: 0,
pageLayoutId: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.TEAM_DASHBOARD),
workspaceId,
universalIdentifier: v4(),
applicationId,
overrides: null,
},
{
id: generateSeedId(workspaceId, PAGE_LAYOUT_TAB_SEEDS.TEAM_METRICS),
title: 'Tasks & Activity',
position: 1,
pageLayoutId: generateSeedId(workspaceId, PAGE_LAYOUT_SEEDS.TEAM_DASHBOARD),
workspaceId,
universalIdentifier: v4(),
applicationId,
overrides: null,
},
];
buildTab(
PAGE_LAYOUT_TAB_SEEDS.TEAM_OVERVIEW,
'Team & People',
0,
PAGE_LAYOUT_SEEDS.TEAM_DASHBOARD,
),
buildTab(
PAGE_LAYOUT_TAB_SEEDS.TEAM_METRICS,
'Tasks & Activity',
1,
PAGE_LAYOUT_SEEDS.TEAM_DASHBOARD,
),
buildTab(
PAGE_LAYOUT_TAB_SEEDS.DOCUMENTATION,
'Documentation',
0,
PAGE_LAYOUT_SEEDS.DOCUMENTATION_STANDALONE_PAGE,
),
];
};
@@ -6,6 +6,9 @@ import {
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { fromPageLayoutWidgetConfigurationToUniversalConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-configuration-to-universal-configuration.util';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { AxisNameDisplay } from 'src/engine/metadata-modules/page-layout-widget/enums/axis-name-display.enum';
import { BarChartLayout } from 'src/engine/metadata-modules/page-layout-widget/enums/bar-chart-layout.enum';
@@ -18,6 +21,77 @@ import { PAGE_LAYOUT_WIDGET_SEEDS } from 'src/engine/workspace-manager/dev-seede
import { type SeederFlatPageLayoutWidget } from 'src/engine/workspace-manager/dev-seeder/core/types/seeder-flat-page-layout-widget.type';
import { generateSeedId } from 'src/engine/workspace-manager/dev-seeder/core/utils/generate-seed-id.util';
import { getPageLayoutWidgetDataSeedsV2 } from 'src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds-v2.util';
import {
getSeedFrontComponentDefinitions,
getSeedFrontComponentIds,
} from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-front-component-definitions.util';
export const getPageLayoutWidgetFlatEntitySeeds = ({
workspaceId,
flatApplication,
objectMetadataItems,
}: {
workspaceId: string;
flatApplication: FlatApplication;
objectMetadataItems: ObjectMetadataEntity[];
}): FlatPageLayoutWidget[] => {
const seeds = getPageLayoutWidgetDataSeeds(workspaceId, objectMetadataItems);
const now = new Date().toISOString();
const fieldMetadataUniversalIdentifierById: Record<string, string> = {};
for (const objectMetadata of objectMetadataItems) {
if (!isDefined(objectMetadata.fields)) {
continue;
}
for (const field of objectMetadata.fields) {
fieldMetadataUniversalIdentifierById[field.id] =
field.universalIdentifier;
}
}
const frontComponentUniversalIdentifierById: Record<string, string> = {};
for (const definition of getSeedFrontComponentDefinitions(workspaceId)) {
frontComponentUniversalIdentifierById[definition.id] =
definition.universalIdentifier;
}
return seeds.map((seed) => {
const objectMetadata = isDefined(seed.objectMetadataId)
? objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.id === seed.objectMetadataId,
)
: undefined;
const universalConfiguration =
fromPageLayoutWidgetConfigurationToUniversalConfiguration({
configuration: seed.configuration,
fieldMetadataUniversalIdentifierById,
frontComponentUniversalIdentifierById,
});
return {
...seed,
universalIdentifier: seed.id,
applicationId: flatApplication.id,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
workspaceId,
pageLayoutTabUniversalIdentifier: seed.pageLayoutTabId,
objectMetadataUniversalIdentifier:
objectMetadata?.universalIdentifier ?? null,
universalConfiguration,
conditionalDisplay: null,
conditionalAvailabilityExpression: null,
isActive: true,
createdAt: now,
updatedAt: now,
deletedAt: null,
universalOverrides: null,
} as FlatPageLayoutWidget;
});
};
const getFieldId = (
object: ObjectMetadataEntity | undefined,
@@ -299,13 +373,13 @@ export const getPageLayoutWidgetDataSeeds = (
),
title: 'Companies by Size (Stacked by City)',
type: WidgetType.GRAPH,
gridPosition: { row: 0, column: 8, rowSpan: 10, columnSpan: 8 },
gridPosition: { row: 0, column: 6, rowSpan: 10, columnSpan: 6 },
position: {
layoutMode: PageLayoutTabLayoutMode.GRID,
row: 0,
column: 8,
column: 6,
rowSpan: 10,
columnSpan: 8,
columnSpan: 6,
},
configuration: {
configurationType: WidgetConfigurationType.BAR_CHART,
@@ -516,11 +590,38 @@ export const getPageLayoutWidgetDataSeeds = (
},
configuration: {
configurationType: WidgetConfigurationType.FRONT_COMPONENT,
frontComponentId: '6cdf2607-4b28-40e6-8c53-cc06799ddc88',
frontComponentId: getSeedFrontComponentIds(workspaceId).helloWorldId,
} as const,
objectMetadataId: null,
overrides: null,
} satisfies SeederFlatPageLayoutWidget,
{
id: generateSeedId(
workspaceId,
PAGE_LAYOUT_WIDGET_SEEDS.DOCUMENTATION_IFRAME,
),
pageLayoutTabId: generateSeedId(
workspaceId,
PAGE_LAYOUT_TAB_SEEDS.DOCUMENTATION,
),
title: 'Twenty Star History',
type: WidgetType.IFRAME,
gridPosition: { row: 0, column: 0, rowSpan: 12, columnSpan: 12 },
position: {
layoutMode: PageLayoutTabLayoutMode.GRID,
row: 0,
column: 0,
rowSpan: 12,
columnSpan: 12,
},
configuration: {
configurationType: WidgetConfigurationType.IFRAME,
url: 'https://www.star-history.com/?repos=twentyhq%2Ftwenty&type=date&legend=top-left',
},
objectMetadataId: null,
overrides: null,
} satisfies SeederFlatPageLayoutWidget,
].filter(isDefined);
const v2Widgets = getPageLayoutWidgetDataSeedsV2(
@@ -1,39 +0,0 @@
import { type DataSource } from 'typeorm';
import { getPageLayoutTabDataSeeds } from 'src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-tab-data-seeds.util';
export const seedPageLayoutTabs = async ({
applicationId,
dataSource,
schemaName,
workspaceId,
}: {
dataSource: DataSource;
schemaName: string;
applicationId: string;
workspaceId: string;
}) => {
const pageLayoutTabs = getPageLayoutTabDataSeeds({
workspaceId,
applicationId,
});
if (pageLayoutTabs.length > 0) {
await dataSource
.createQueryBuilder()
.insert()
.into(`${schemaName}.pageLayoutTab`, [
'id',
'title',
'position',
'pageLayoutId',
'workspaceId',
'universalIdentifier',
'applicationId',
'overrides',
])
.values(pageLayoutTabs)
.orIgnore()
.execute();
}
};
@@ -1,68 +0,0 @@
import { isDefined } from 'class-validator';
import { type DataSource } from 'typeorm';
import { v4 } from 'uuid';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { validateWidgetConfigurationInput } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-widget-configuration-input.util';
import { getPageLayoutWidgetDataSeeds } from 'src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds.util';
export const seedPageLayoutWidgets = async ({
dataSource,
schemaName,
workspaceId,
objectMetadataItems,
applicationId,
}: {
dataSource: DataSource;
schemaName: string;
workspaceId: string;
objectMetadataItems: ObjectMetadataEntity[];
applicationId: string;
}) => {
const widgetSeeds = getPageLayoutWidgetDataSeeds(
workspaceId,
objectMetadataItems,
);
const pageLayoutWidgets = widgetSeeds.map((widget) => {
if (isDefined(widget.configuration)) {
validateWidgetConfigurationInput({
configuration: widget.configuration,
});
}
return {
...widget,
workspaceId,
gridPosition: widget.gridPosition,
position: widget.position,
configuration: widget.configuration,
universalIdentifier: v4(),
applicationId,
overrides: widget.overrides ?? null,
};
});
if (pageLayoutWidgets.length > 0) {
await dataSource
.createQueryBuilder()
.insert()
.into(`${schemaName}.pageLayoutWidget`, [
'id',
'pageLayoutTabId',
'title',
'type',
'gridPosition',
'position',
'configuration',
'objectMetadataId',
'workspaceId',
'universalIdentifier',
'applicationId',
'overrides',
])
.values(pageLayoutWidgets)
.orIgnore()
.execute();
}
};
@@ -1,30 +1,78 @@
import { type DataSource } from 'typeorm';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { getNavigationMenuItemFlatEntitySeeds } from 'src/engine/workspace-manager/dev-seeder/core/utils/get-navigation-menu-item-data-seeds.util';
import { getPageLayoutFlatEntitySeeds } from 'src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-data-seeds.util';
import { getPageLayoutTabFlatEntitySeeds } from 'src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-tab-data-seeds.util';
import { getPageLayoutWidgetFlatEntitySeeds } from 'src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds.util';
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
import { type WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
import { getPageLayoutDataSeeds } from 'src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-data-seeds.util';
export const seedPageLayouts = async ({
workspaceId,
flatApplication,
objectMetadataItems,
workspaceMigrationValidateBuildAndRunService,
}: {
workspaceId: string;
flatApplication: FlatApplication;
objectMetadataItems: ObjectMetadataEntity[];
workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService;
}): Promise<void> => {
const pageLayouts = getPageLayoutFlatEntitySeeds({
workspaceId,
flatApplication,
});
export const seedPageLayouts = async (
dataSource: DataSource,
schemaName: string,
workspaceId: string,
applicationId: string,
) => {
const pageLayouts = getPageLayoutDataSeeds(workspaceId, applicationId);
const pageLayoutTabs = getPageLayoutTabFlatEntitySeeds({
workspaceId,
flatApplication,
});
if (pageLayouts.length > 0) {
await dataSource
.createQueryBuilder()
.insert()
.into(`${schemaName}.pageLayout`, [
'id',
'name',
'type',
'objectMetadataId',
'workspaceId',
'universalIdentifier',
'applicationId',
])
.values(pageLayouts)
.orIgnore()
.execute();
const pageLayoutWidgets = getPageLayoutWidgetFlatEntitySeeds({
workspaceId,
flatApplication,
objectMetadataItems,
});
const navigationMenuItems = getNavigationMenuItemFlatEntitySeeds({
workspaceId,
flatApplication,
});
const result =
await workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayout: {
flatEntityToCreate: pageLayouts,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayoutTab: {
flatEntityToCreate: pageLayoutTabs,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayoutWidget: {
flatEntityToCreate: pageLayoutWidgets,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
navigationMenuItem: {
flatEntityToCreate: navigationMenuItems,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
},
);
if (result.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
result,
'Failed to seed page layouts, tabs, widgets, and navigation menu items',
);
}
};
@@ -114,11 +114,7 @@ import {
WORKSPACE_MEMBER_DATA_SEED_COLUMNS,
} from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant';
import { TimelineActivitySeederService } from 'src/engine/workspace-manager/dev-seeder/data/services/timeline-activity-seeder.service';
import { PrefillFrontComponentService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/prefill-front-component.service';
import { PrefillLogicFunctionService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/prefill-logic-function.service';
import { prefillFrontComponentCommandMenuItems } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-front-component-command-menu-items.util';
import { getSeedFrontComponentDefinitions } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-front-component-definitions.util';
import { getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionDefinitions } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-code-step-logic-functions.util';
import { prefillWorkflowCommandMenuItems } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-command-menu-items.util';
import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflows.util';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
@@ -293,8 +289,6 @@ export class DevSeederDataService {
private readonly timelineActivitySeederService: TimelineActivitySeederService,
private readonly fileStorageService: FileStorageService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly prefillLogicFunctionService: PrefillLogicFunctionService,
private readonly prefillFrontComponentService: PrefillFrontComponentService,
private readonly applicationService: ApplicationService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
) {}
@@ -324,19 +318,6 @@ export class DevSeederDataService {
const { seeds: attachmentSeeds, fileSeedMetadata: attachmentFileMeta } =
generateAttachmentSeedsForWorkspace(workspaceId);
await this.prefillLogicFunctionService.ensureSeeded({
workspaceId,
definitions:
getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionDefinitions(
workspaceId,
),
});
await this.prefillFrontComponentService.ensureSeeded({
workspaceId,
definitions: getSeedFrontComponentDefinitions(workspaceId),
});
await this.coreDataSource.transaction(
async (entityManager: WorkspaceEntityManager) => {
await this.seedRecordsInBatches({
@@ -1,7 +1,6 @@
import { Injectable, Logger } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { DataSource, Repository } from 'typeorm';
import { v4 } from 'uuid';
@@ -12,38 +11,37 @@ import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twent
import { UpgradeMigrationService } from 'src/engine/core-modules/upgrade/services/upgrade-migration.service';
import { UpgradeSequenceReaderService } from 'src/engine/core-modules/upgrade/services/upgrade-sequence-reader.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { seedBillingCustomers } from 'src/engine/workspace-manager/dev-seeder/core/billing/utils/seed-billing-customers.util';
import { seedBillingSubscriptions } from 'src/engine/workspace-manager/dev-seeder/core/billing/utils/seed-billing-subscriptions.util';
import {
type SeededWorkspacesIds,
SEEDER_CREATE_WORKSPACE_INPUT,
} from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
import { seedBillingCustomers } from 'src/engine/workspace-manager/dev-seeder/core/billing/utils/seed-billing-customers.util';
import { seedBillingSubscriptions } from 'src/engine/workspace-manager/dev-seeder/core/billing/utils/seed-billing-subscriptions.util';
import { DevSeederPermissionsService } from 'src/engine/workspace-manager/dev-seeder/core/services/dev-seeder-permissions.service';
import { seedAgents } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-agents.util';
import { seedApiKeys } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-api-keys.util';
import { seedFeatureFlags } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util';
import { seedMetadataEntities } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-metadata-entities.util';
import { seedPageLayouts } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layouts.util';
import { seedServerId } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-server-id.util';
import { seedUserWorkspaces } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-user-workspaces.util';
import { seedUsers } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-users.util';
import { createWorkspace } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspace.util';
import { seedPageLayoutTabs } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layout-tabs.util';
import { seedPageLayoutWidgets } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layout-widgets.util';
import { seedPageLayouts } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layouts.util';
import { DevSeederDataService } from 'src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service';
import { DevSeederMetadataService } from 'src/engine/workspace-manager/dev-seeder/metadata/services/dev-seeder-metadata.service';
import { PrefillFrontComponentService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/prefill-front-component.service';
import { PrefillLogicFunctionService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/prefill-logic-function.service';
import { getSeedFrontComponentDefinitions } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-front-component-definitions.util';
import { getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionDefinitions } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-code-step-logic-functions.util';
import { TwentyStandardApplicationService } from 'src/engine/workspace-manager/twenty-standard-application/services/twenty-standard-application.service';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
@Injectable()
export class DevSeederService {
private readonly logger = new Logger(DevSeederService.name);
constructor(
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
private readonly twentyConfigService: TwentyConfigService,
@@ -58,6 +56,9 @@ export class DevSeederService {
private readonly sdkClientGenerationService: SdkClientGenerationService,
private readonly upgradeMigrationService: UpgradeMigrationService,
private readonly upgradeSequenceReaderService: UpgradeSequenceReaderService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly prefillFrontComponentService: PrefillFrontComponentService,
private readonly prefillLogicFunctionService: PrefillLogicFunctionService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
@InjectRepository(WorkspaceEntity)
@@ -142,19 +143,6 @@ export class DevSeederService {
light,
});
await seedPageLayouts(
this.coreDataSource,
'core',
workspaceId,
twentyStandardFlatApplication.id,
);
await seedPageLayoutTabs({
applicationId: twentyStandardFlatApplication.id,
workspaceId,
dataSource: this.coreDataSource,
schemaName: 'core',
});
const objectMetadataRepository =
this.coreDataSource.getRepository(ObjectMetadataEntity);
const objectMetadataItems = await objectMetadataRepository.find({
@@ -162,24 +150,26 @@ export class DevSeederService {
relations: { fields: true },
});
await seedPageLayoutWidgets({
dataSource: this.coreDataSource,
schemaName: 'core',
await this.prefillLogicFunctionService.ensureSeeded({
workspaceId,
objectMetadataItems,
applicationId: twentyStandardFlatApplication.id,
definitions:
getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionDefinitions(
workspaceId,
),
});
const relatedPageLayoutCacheKeysToInvalidate = [
...getMetadataRelatedMetadataNames(ALL_METADATA_NAME.pageLayout),
...getMetadataRelatedMetadataNames(ALL_METADATA_NAME.pageLayoutTab),
...getMetadataRelatedMetadataNames(ALL_METADATA_NAME.pageLayoutWidget),
].map(getMetadataFlatEntityMapsKey);
await this.workspaceCacheService.invalidateAndRecompute(
await this.prefillFrontComponentService.ensureSeeded({
workspaceId,
relatedPageLayoutCacheKeysToInvalidate,
);
definitions: getSeedFrontComponentDefinitions(workspaceId),
});
await seedPageLayouts({
workspaceId,
flatApplication: twentyStandardFlatApplication,
objectMetadataItems,
workspaceMigrationValidateBuildAndRunService:
this.workspaceMigrationValidateBuildAndRunService,
});
await this.devSeederDataService.seed({
schemaName,