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
@@ -62,6 +62,7 @@ export const PinnedCommandMenuItemButtonsEditMode = () => {
() =>
new Set<CommandMenuItemAvailabilityType>([
CommandMenuItemAvailabilityType.GLOBAL,
CommandMenuItemAvailabilityType.GLOBAL_OBJECT_CONTEXT,
mainContextStoreHasSelectedRecords
? CommandMenuItemAvailabilityType.RECORD_SELECTION
: CommandMenuItemAvailabilityType.FALLBACK,
@@ -21,7 +21,7 @@ import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomState
import { type DropResult } from '@hello-pangea/dnd';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { CommandMenuContextApiPageType } from 'twenty-shared/types';
import { ContextStorePageType } from 'twenty-shared/types';
import {
interpolateCommandMenuItemTemplate,
isDefined,
@@ -71,9 +71,13 @@ export const SidePanelCommandMenuItemEditPage = () => {
const currentObjectMetadataItemId =
commandMenuContextApi.objectMetadataItem.id;
const hasObjectContext = isDefined(currentObjectMetadataItemId);
const isRecordPage =
commandMenuContextApi.pageType ===
CommandMenuContextApiPageType.RECORD_PAGE;
commandMenuContextApi.pageType === ContextStorePageType.Record;
const isIndexPage =
commandMenuContextApi.pageType === ContextStorePageType.Index;
const mainContextStoreHasSelectedRecords = useAtomStateValue(
mainContextStoreHasSelectedRecordsSelector,
@@ -93,6 +97,9 @@ export const SidePanelCommandMenuItemEditPage = () => {
const allowedAvailabilityTypes = new Set<CommandMenuItemAvailabilityType>([
CommandMenuItemAvailabilityType.GLOBAL,
...(isIndexPage || isRecordPage
? [CommandMenuItemAvailabilityType.GLOBAL_OBJECT_CONTEXT]
: []),
mainContextStoreHasSelectedRecords
? CommandMenuItemAvailabilityType.RECORD_SELECTION
: CommandMenuItemAvailabilityType.FALLBACK,
@@ -231,11 +238,13 @@ export const SidePanelCommandMenuItemEditPage = () => {
return (
<StyledContainer data-click-outside-id={COMMAND_MENU_CLICK_OUTSIDE_ID}>
<StyledViewbar>
<CommandMenuItemEditRecordSelectionDropdown
isRecordPage={isRecordPage}
/>
</StyledViewbar>
{hasObjectContext && (
<StyledViewbar>
<CommandMenuItemEditRecordSelectionDropdown
isRecordPage={isRecordPage}
/>
</StyledViewbar>
)}
<StyledContent>
<SidePanelList selectableItemIds={selectableItemIds}>
<SidePanelGroup heading={t`Pinned`}>