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
@@ -949,12 +949,6 @@ export class WorkspaceMigrationBuildOrchestratorService {
...aggregatedOrchestratorActionsReport.commandMenuItem.update,
///
// Navigation Menu Items
...aggregatedOrchestratorActionsReport.navigationMenuItem.delete,
...aggregatedOrchestratorActionsReport.navigationMenuItem.create,
...aggregatedOrchestratorActionsReport.navigationMenuItem.update,
///
// Page layouts
...aggregatedOrchestratorActionsReport.pageLayout.delete,
...aggregatedOrchestratorActionsReport.pageLayout.create,
@@ -973,6 +967,12 @@ export class WorkspaceMigrationBuildOrchestratorService {
...aggregatedOrchestratorActionsReport.pageLayoutWidget.update,
///
// Navigation Menu Items
...aggregatedOrchestratorActionsReport.navigationMenuItem.delete,
...aggregatedOrchestratorActionsReport.navigationMenuItem.create,
...aggregatedOrchestratorActionsReport.navigationMenuItem.update,
///
// Row level permission predicate groups
...aggregatedOrchestratorActionsReport
.rowLevelPermissionPredicateGroup.delete,
@@ -28,6 +28,7 @@ export class FlatNavigationMenuItemValidatorService {
hasTargetObjectMetadataId,
hasViewId,
hasLink,
hasPageLayoutId,
name,
}: {
type: NavigationMenuItemType | null | undefined;
@@ -35,6 +36,7 @@ export class FlatNavigationMenuItemValidatorService {
hasTargetObjectMetadataId: boolean;
hasViewId: boolean;
hasLink: boolean;
hasPageLayoutId: boolean;
name: string | null | undefined;
}): FlatEntityValidationError<NavigationMenuItemExceptionCode>[] {
const errors: FlatEntityValidationError<NavigationMenuItemExceptionCode>[] =
@@ -96,6 +98,15 @@ export class FlatNavigationMenuItemValidatorService {
});
}
break;
case NavigationMenuItemType.PAGE_LAYOUT:
if (!hasPageLayoutId) {
errors.push({
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
message: t`pageLayoutId is required for PAGE_LAYOUT type`,
userFriendlyMessage: msg`pageLayoutId is required for PAGE_LAYOUT type`,
});
}
break;
}
return errors;
@@ -188,6 +199,9 @@ export class FlatNavigationMenuItemValidatorService {
hasLink:
isDefined(flatNavigationMenuItem.link) &&
isNonEmptyString(flatNavigationMenuItem.link),
hasPageLayoutId: isDefined(
flatNavigationMenuItem.pageLayoutUniversalIdentifier,
),
name: flatNavigationMenuItem.name,
});
@@ -321,6 +335,9 @@ export class FlatNavigationMenuItemValidatorService {
),
hasViewId: isDefined(toFlatNavigationMenuItem.viewUniversalIdentifier),
hasLink: isNonEmptyString((toFlatNavigationMenuItem.link ?? '').trim()),
hasPageLayoutId: isDefined(
toFlatNavigationMenuItem.pageLayoutUniversalIdentifier,
),
name: isDefined(nameUpdate) ? nameUpdate : toFlatNavigationMenuItem.name,
});
@@ -30,7 +30,7 @@ export class CreateNavigationMenuItemActionHandlerService extends WorkspaceMigra
flatApplication,
workspaceId,
}: WorkspaceMigrationActionRunnerArgs<UniversalCreateNavigationMenuItemAction>): Promise<FlatCreateNavigationMenuItemAction> {
const { targetObjectMetadataId, folderId, viewId } =
const { targetObjectMetadataId, folderId, viewId, pageLayoutId } =
resolveUniversalRelationIdentifiersToIds({
flatEntityMaps: allFlatEntityMaps,
metadataName: action.metadataName,
@@ -49,6 +49,7 @@ export class CreateNavigationMenuItemActionHandlerService extends WorkspaceMigra
targetObjectMetadataId,
folderId,
viewId,
pageLayoutId,
applicationId: flatApplication.id,
id: action.id ?? v4(),
workspaceId,