From a375a9ae0a6e7a61f0e200d8e54d158952f8d90b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 09:14:13 +0000 Subject: [PATCH] docs(skills): rewrite console-development.md to the post-cccdf84d reality and sync its eval (#3713) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five of the seven `pages/system/` files the guide taught do not exist, and a whole section documented `MetadataManagerPage` — deleted, zero hits repo-wide — including a route example and a `listComponent` extension point on a registry file (`config/metadataTypeRegistry.ts`) that never existed under `apps/console/src/`. - Directory tree rewritten to the six real `pages/system/` pages, plus a relocation table for everything that left `apps/console` in cccdf84d7. - New "Retired names" correction anchor: each dead symbol appears exactly once in the guide, immediately followed by its verdict and live replacement. - The registry chapter now documents the real engine — `packages/app-shell/src/views/metadata-admin/registry.ts`, `MetadataResourceConfig`, `registerMetadataResource()`, and the actual `MetadataResourceListPage` / `MetadataResourceEditPage` flows. - Routing section replaced with the real two-file split, the engine's canonical `metadata/:type…` routes, and the four `SystemObjectRedirect` legs (#3655's permissions leg is ruled but not yet on main, and is labelled as such). - Reference-assembly positioning added up front, citing cccdf84d7. - evals/console-development.json: expected_output no longer bakes in `MetadataManagerPage`; the phantom `MetadataTypeRegistry`, `MetadataDetailPage`, `pageSchemaFactory` and `registerWidgets` assertions are replaced with tokens that resolve to real code, and the dead API names move to `must_not_contain` so a stale answer now fails instead of passing. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt --- .../objectui/evals/console-development.json | 29 +- skills/objectui/guides/console-development.md | 373 ++++++++++++------ 2 files changed, 279 insertions(+), 123 deletions(-) diff --git a/skills/objectui/evals/console-development.json b/skills/objectui/evals/console-development.json index a2427919d8..43f3b20e3e 100644 --- a/skills/objectui/evals/console-development.json +++ b/skills/objectui/evals/console-development.json @@ -3,17 +3,20 @@ "evals": [ { "id": 1, - "prompt": "I need to add a new 'Workflow' metadata type to the console admin panel. It should show in the system hub, have a list page with name/status/trigger columns, and a create dialog with name and trigger object fields.", - "expected_output": "Provides MetadataTypeRegistry configuration with columns, formFields, icon. Shows where to add the registration and how it automatically appears in SystemHubPage and MetadataManagerPage.", + "prompt": "I need to add a new 'Workflow' metadata type to the console admin panel. It should be reachable from the admin navigation, list items with name/status/trigger-object columns, and ask for name plus trigger object when creating one.", + "expected_output": "Explains that the metadata-admin engine already delivers list/create/edit/history once the type exists in the framework's spec and type registry, so the UI-side work is an optional registerMetadataResource() override supplying listColumns and createFields. Points at packages/app-shell/src/views/metadata-admin/registry.ts, and reaches the surface through the app's navigation rather than a bespoke card or page.", "files": [], "assertions": { "must_contain": [ - "MetadataTypeRegistry", - "columns", - "formFields", - "SystemHubPage" + "registerMetadataResource", + "listColumns", + "createFields", + "metadata-admin", + "navigation" ], "must_not_contain": [ + "registerMetadataType", + "pageSchemaFactory", "new standalone page", "/system/objects" ] @@ -22,16 +25,18 @@ { "id": 2, "prompt": "I want to create a custom detail page for the 'workflow' metadata type that has tabs for Settings, Steps, and History, similar to how object detail pages work.", - "expected_output": "Provides pageSchemaFactory function, custom widget components for each tab, registerWidgets file, and MetadataTypeRegistry wiring with pageSchemaFactory.", + "expected_output": "Provides a PageSchema factory, self-contained widget components for each tab registered in ComponentRegistry, and an EditPage component that renders the factory's schema through SchemaRenderer — wired to the type by registerMetadataResource({ type: 'workflow', EditPage }). Mirrors apps/console/src/schemas/objectDetailPageSchema.ts plus components/schema/registerObjectDetailWidgets.ts.", "files": [], "assertions": { "must_contain": [ - "pageSchemaFactory", - "registerWidgets", - "MetadataDetailPage", - "tabs" + "ComponentRegistry", + "SchemaRenderer", + "EditPage", + "registerMetadataResource" ], - "must_not_contain": [] + "must_not_contain": [ + "pageSchemaFactory" + ] } }, { diff --git a/skills/objectui/guides/console-development.md b/skills/objectui/guides/console-development.md index b0d57a2faa..56f46c55b3 100644 --- a/skills/objectui/guides/console-development.md +++ b/skills/objectui/guides/console-development.md @@ -1,12 +1,26 @@ --- name: objectui-console-development -description: Develop features for the Object UI Console application — the reference admin interface. Use this skill when the user works on apps/console code, adds admin pages, modifies the metadata management system, extends the MetadataTypeRegistry, creates schema-driven detail pages with PageSchema factories, modifies the UnifiedSidebar or navigation, works with ConsoleLayout/HomeLayout, builds system hub pages, or debugs console-specific routing. Also applies when the user mentions metadata types, object management, admin panel, system settings, app management, or console navigation patterns. +description: Develop features for the Object UI Console application — the reference admin interface. Use this skill when the user works on apps/console code, adds admin pages, modifies the metadata management system, extends the metadata-admin resource registry, creates schema-driven detail pages with PageSchema factories, modifies the UnifiedSidebar or navigation, works with ConsoleLayout/HomeLayout, builds system hub pages, or debugs console-specific routing. Also applies when the user mentions metadata types, object management, admin panel, system settings, app management, or console navigation patterns. --- # ObjectUI Console Development Use this skill when working on the Console application (`apps/console/`), the reference admin interface for Object UI. The Console demonstrates schema-driven patterns at scale and serves as the blueprint for enterprise admin interfaces. +> **`apps/console` is a reference assembly, not the engine.** Commit `cccdf84d7` +> ("Slim apps/console for third-party customisation") moved the shell, the layouts, +> the home pages, the navigation and the whole metadata admin into +> `@object-ui/app-shell`, moved the designer pages into +> `@object-ui/plugin-designer`, and left `apps/console` as a thin host that mostly +> declares routes. Two consequences for anyone reading this guide: +> +> - **Look in `packages/app-shell` first.** Most symbols you want to change are +> there, not under `apps/console/src/`. Grep before you assume a path. +> - **Third parties fork the template, not this app** — `examples/console-starter` +> wires `ConsoleShell` + host routes in ~100 LOC. Adding a surface to +> `apps/console` only benefits this repo's own deployment; adding it to +> `app-shell` (or to an app's `navigation[]` metadata) benefits every host. + ## Console architecture overview ### App entry and provider stack @@ -47,104 +61,167 @@ host app (see `guides/project-setup.md` → "Runtime & integration packages"). ``` apps/console/src/pages/ -├── home/ # Workspace hub -│ ├── HomeLayout.tsx # Home shell (AppShell + sidebar, sets nav context) -│ ├── HomePage.tsx # Workspace dashboard -│ ├── QuickActions.tsx # Quick action buttons -│ ├── RecentApps.tsx # Recently-accessed apps -│ └── AppCard.tsx # App preview card -├── system/ # Admin panel -│ ├── SystemHubPage.tsx # Card-based admin hub -│ ├── MetadataManagerPage.tsx # Registry-driven list page -│ ├── MetadataDetailPage.tsx # Registry-driven detail page -│ ├── AppManagementPage.tsx -│ ├── UserManagementPage.tsx -│ ├── RoleManagementPage.tsx -│ └── PermissionManagementPage.tsx -├── CreateAppPage.tsx -├── EditAppPage.tsx -├── PageDesignPage.tsx -└── DashboardDesignPage.tsx +├── auth/ # Login, register, reset/set password, verify email, +│ # OAuth consent, device auth, accept invitation +├── developer/ # DeveloperHubPage, ApiConsolePage, FlowRunsPage, +│ # PublicFormsPage, IntegrationsPage +├── settings/ # SettingsHub + SettingsView (`system/settings/:namespace`) +├── system/ # Admin panel — exactly six pages +│ ├── SystemHubPage.tsx # Card-based admin hub (@deprecated — see below) +│ ├── AppManagementPage.tsx # App CRUD +│ ├── ProfilePage.tsx # Signed-in user's own profile +│ ├── AuditLogPage.tsx # Read-only audit trail (hand-written REST fetch, +│ │ # deliberately not an ObjectView) +│ ├── ApprovalsInboxPage.tsx # Approvals awaiting the current user +│ └── AiPendingActionsPage.tsx # AI-proposed actions awaiting confirmation +├── DocsLayout.tsx / AppDocsIndex.tsx / DocsSlug.tsx / DocPage.tsx / BookPage.tsx +│ # ADR-0048 / ADR-0046 §6 — package docs reader +└── SharedRecordPage.tsx # Public share-link landing ``` -## Metadata Type Registry +Where the rest went (all of it left `apps/console` in `cccdf84d7`): + +| Looking for | Today's home | +|---|---| +| `home/` (`HomeLayout`, `HomePage`, `QuickActions`, `RecentApps`, `AppCard`) | `packages/app-shell/src/console/home/` | +| `CreateAppPage`, `EditAppPage`, `DashboardDesignPage` | `packages/plugin-designer/src/pages/` | +| `UnifiedSidebar`, `ConsoleLayout` | `packages/app-shell/src/layout/` | +| The registry-driven metadata pages | `packages/app-shell/src/views/metadata-admin/` (see next section) | + +### Retired names — do not import these + +Earlier versions of this guide taught seven `pages/system/` files. Five of them are +gone; searching for them wastes a lap, and writing code against them does not +compile. Each is listed here **once**, with what replaced it: + +| Retired symbol | Verdict | +|---|---| +| `MetadataManagerPage` | **Deleted.** Registry-driven list is now `MetadataResourceListPage` in `packages/app-shell/src/views/metadata-admin/ResourceListPage.tsx`. | +| `MetadataDetailPage` | **Deleted.** Registry-driven editor is now `MetadataResourceEditPage` in `.../ResourceEditPage.tsx`. | +| `UserManagementPage` | **Deleted** (`cccdf84d7`, whose message records where these objects went: "contributed by framework plugins (plugin-auth, -security, -audit) into the Setup app navigation"). The URL is now a redirect to `sys_user` — see "Routing patterns". | +| `RoleManagementPage` | **Deleted** (`cccdf84d7`). ADR-0090 D3 renamed `sys_role` to `sys_position`; the URL redirects there. | +| `PermissionManagementPage` | **Deleted** (`cccdf84d7`, 26 lines). Permissions are edited through `PermissionMatrixEditor`, registered as the `permission` type's `EditPage`. | +| `config/metadataTypeRegistry.ts` | **Never existed under `apps/console/src/`.** The real registry is `packages/app-shell/src/views/metadata-admin/registry.ts`. | +| `MetadataTypeConfig`, `registerMetadataType`, `getMetadataTypeConfig`, `pageSchemaFactory`, `listComponent`, `MetadataListComponentProps` | **No such API anywhere in the repo** (zero hits). The live equivalents are `MetadataResourceConfig`, `registerMetadataResource()`, `getMetadataResource()`, and the `ListPage` / `EditPage` / `CreatePage` component overrides. | + +## Metadata resource registry (metadata-admin engine) + +The registry lives in `@object-ui/app-shell`, not in the console: +`packages/app-shell/src/views/metadata-admin/registry.ts`. + +The important inversion versus the old page-per-type design: the engine drives **all** +metadata types from one `ListPage` / `EditPage` / `HistoryPage` shell, and the default +form is generated from the JSONSchema the framework serves on `/api/v1/meta/types`. A +type does not need a registry entry at all to be listable and editable. Registering one +only **overrides** defaults. -The registry at `apps/console/src/config/metadataTypeRegistry.ts` is the central configuration for all admin-managed entity types (app, dashboard, page, report, object, etc.). +The contract for "add a new metadata type" is therefore mostly *not* a UI change: -### MetadataTypeConfig interface +1. Define its Zod schema in the framework's `packages/spec/src//`. +2. Add it to the framework's `DEFAULT_METADATA_TYPE_REGISTRY`. +3. Done — it appears in the Setup app's Metadata Directory with working + list / create / edit / history. + +### MetadataResourceConfig — the override surface + +Every field is optional. The ones you reach for most (see `registry.ts` for the full +~30-field interface, which is documented field by field): ```typescript -interface MetadataTypeConfig { - type: string; // API metadata category ('dashboard', 'page', 'object') - label: string; // Singular display name - pluralLabel: string; // Plural display name - icon: string; // Lucide icon name - columns?: MetadataColumnDef[]; // Columns for list view - formFields?: MetadataFormFieldDef[]; // Fields for create/edit dialog - customRoute?: string; // Override hub card link URL - pageSchemaFactory?: (name: string, item?: any) => PageSchema; // Schema-driven detail page - countSource?: 'metadata' | 'dataSource'; // Where to get item count - editable?: boolean; // Enable CRUD (default: true) - listComponent?: React.ComponentType; // Custom list UI +interface MetadataResourceConfig { + type: string; // 'view', 'flow', 'permission', … + label?: string; // falls back to the server label, then to `type` + description?: string; // shown in the page hero + iconName?: string; // Lucide icon name + domain?: MetadataDomain; // 'data' | 'ui' | 'automation' | 'security' | … + primaryKey?: string; // default 'name' + identityField?: string; // URL slug + body key; default 'name' + searchableFields?: string[]; // default ['name','label','description'] + listColumns?: Array<{ key: string; label: string; width?: string; + render?: (value: unknown, item: Record) => ReactNode }>; + listFilter?: (item: Record) => boolean; // false hides the row + ListPage?: ComponentType<{ type: string }>; // bypass the generic list + EditPage?: ComponentType<{ type: string; name: string }>; // bypass the generic editor + CreatePage?: ComponentType<{ type: string }>; // bypass the generic create + createFields?: string[]; // which fields the create form asks for + createDerive?: CreateDeriveRule[]; // e.g. label -> name via 'slugify' + createDefaults?: Record; // shallow-merged into the PUT body + createBuildBody?: (draft: Record) => Record; + toDraft?: (item: Record) => Record; // wire -> editor + fromDraft?: (draft: Record) => Record; // editor -> wire + defaultSchema?: Record; // used only when the server serves none + supportsHistory?: boolean; // default true + anchors?: MetadataAnchor[]; // powers the parent's Related tab } ``` -### How MetadataManagerPage works +Registration is idempotent and **merging**: calling `registerMetadataResource()` twice for +the same `type` merges the new fields over the old, so a bespoke editor and the +generic-engine defaults can be registered independently. + +### How MetadataResourceListPage works + +`packages/app-shell/src/views/metadata-admin/ResourceListPage.tsx`: -Instead of hardcoding pages per type, `MetadataManagerPage` reads the registry config: +1. Resolves `type` from the `type` prop, else from the `:type` route param. +2. `getMetadataResource(type)` — if the entry has a `ListPage`, renders it and returns + (done before any other hook, so the hook count stays stable across type switches). +3. Otherwise `resolveResourceConfig(type, serverEntry)` merges the registry entry with the + `/meta/types` row (server `label` / `description` / `domain` fill the gaps). +4. Fetches `/meta/:type`, then filters by search (`searchableFields`), source/overlay, and + package scope — plus `listFilter` if the type declares one. +5. Renders `listColumns`, falling back to columns inferred from `primaryKey`. Each row + links to its editor at `./:name?type=…`. -1. Gets `metadataType` from URL params -2. Looks up `getMetadataTypeConfig(metadataType)` -3. Uses `config.columns` to render the list table -4. Uses `config.formFields` to render create/edit dialog -5. If `config.listComponent` is set, renders that instead of the default grid +### How MetadataResourceEditPage works -### How MetadataDetailPage works +`.../ResourceEditPage.tsx`, one component for both edit and create (`createMode`): -1. Gets `metadataType` and `itemName` from URL params -2. Looks up config from registry -3. If `config.pageSchemaFactory` exists: generates PageSchema → renders via `SchemaRenderer` -4. Otherwise: renders a generic detail card +1. A registered `EditPage` (edit mode) or `CreatePage` (create mode) short-circuits + everything below — that is how `PermissionMatrixEditor` replaces the form for + `type: 'permission'`. +2. Otherwise it picks a schema: `createSchema` in create mode, else the `/meta/types` + row's `schema`, else the registry's `defaultSchema`. +3. Fetches the layered view (`?layers=true`) so code / overlay / effective are all + visible, and renders `SchemaForm` against that schema. +4. Save → PUT. A `409 destructive_change` opens a confirmation dialog and retries with + `?force=true`. Reset overlay → DELETE. The References tab calls `client.references()` + so an admin sees the back-pointers before deleting. -### Adding a new metadata type +### Adding a registry override ```typescript -// In metadataTypeRegistry.ts -registerMetadataType({ - type: 'workflow', - label: 'Workflow', - pluralLabel: 'Workflows', - icon: 'git-branch', - columns: [ - { key: 'name', label: 'Name' }, - { key: 'status', label: 'Status', width: '100px' }, - { key: 'triggerObject', label: 'Object' }, +// packages/app-shell/src/services/builtinComponents.tsx +registerMetadataResource({ + type: 'permission', + label: 'Permission sets', + description: 'Object-level CRUD + VAMA + lifecycle permissions, and field-level R/W.', + domain: 'security', + EditPage: PermissionMatrixEditPage, // bespoke grid replaces the generic AutoForm + searchableFields: ['name', 'label'], + listColumns: [ + { key: 'name', label: 'Name', width: '30%' }, + { key: 'label', label: 'Label', width: '30%' }, + { key: 'managedBy', label: 'Source', width: '15%', + render: (v) => (v === 'package' ? 'Package' : 'Custom') }, ], - formFields: [ - { key: 'name', label: 'Name', type: 'text', required: true }, - { key: 'triggerObject', label: 'Trigger Object', type: 'select', options: objectOptions }, - { key: 'active', label: 'Active', type: 'boolean' }, - ], - editable: true, }); ``` -### MetadataFormFieldDef types - -Supported form field types: -- `'text'` — text input -- `'textarea'` — multiline text -- `'select'` — dropdown with options -- `'number'` — numeric input -- `'boolean'` — Switch toggle (stored as `'true'`/`'false'` strings) - ## Schema-driven detail pages (PageSchema) ### The pattern 1. **Schema factory** generates a `PageSchema` from entity name/data 2. **Custom widgets** are self-contained React components registered in `ComponentRegistry` -3. **MetadataDetailPage** calls the factory and renders via `SchemaRenderer` +3. **A registered `EditPage`** calls the factory and renders the result via `SchemaRenderer` + +> Measured status: the factory and its widgets exist and are registered +> (`main.tsx` imports `registerObjectDetailWidgets`), but **nothing in the repo calls +> `buildObjectDetailPageSchema()` today** — its only caller was the registry-driven +> detail page retired above. Its own docblock records the intended wiring: "Render via +> SchemaRenderer (e.g. inside a custom metadata-admin EditPage)." Treat the pattern +> below as the recipe for a *new* bespoke editor, not as a description of a live route. ### Object detail page example @@ -207,11 +284,21 @@ ComponentRegistry.register('my-detail-tabs', MyDetailTabsWidget, { }); ``` -4. Wire into MetadataTypeRegistry: +4. Wrap the factory in an `EditPage` component and register it as that type's override. + There is no declarative "give me a schema factory" hook — the registry takes a React + component, so the factory call happens inside it: + ```typescript -registerMetadataType({ +import { SchemaRenderer } from '@object-ui/react'; +import { registerMetadataResource } from '@object-ui/app-shell'; + +function MyEntityEditPage({ name }: { type: string; name: string }) { + return ; +} + +registerMetadataResource({ type: 'my-entity', - pageSchemaFactory: buildMyDetailPageSchema, + EditPage: MyEntityEditPage, // ... }); ``` @@ -259,33 +346,70 @@ function HomeLayout({ children }) { ### Routing patterns -```typescript -// apps/console/src/App.tsx routes (simplified) - - {/* Home */} - }> - } /> - - - {/* App context */} - }> - } /> - } /> - } /> - - - {/* Admin */} - }> - } /> - } /> - } /> - - - {/* Legacy redirects */} - } /> - } /> - +Routes are split across two files, and that split is the thing to get right: + +- **`apps/console/src/App.tsx`** — the outer skeleton only (auth pages, `/home`, + `/organizations`, `/create-app`, `/apps/:appName/*`). +- **`packages/app-shell/src/console/AppContent.tsx`** (`DefaultAppContent`) — everything + *inside* one app, including the metadata-admin engine's own routes. It declares two + route tables: one for when an app is active, one for the zero-app case + (`extraRoutesNoApp`). +- **`apps/console/src/AppContent.tsx`** — a thin wrapper that passes a + `systemRoutes` JSX fragment into `DefaultAppContent`. Console-specific pages and + legacy-URL redirects live in that fragment; it is exported so tests mount the real + fragment instead of a hand-copied transcription. + +The engine's canonical metadata routes (declared by `DefaultAppContent`, relative to +`/apps/:appName`): + ``` +metadata -> MetadataDirectoryPage +metadata/_diagnostics -> MetadataDiagnosticsPage +metadata/:type -> MetadataResourceListPage +metadata/:type/new -> MetadataResourceEditPage (createMode) +metadata/:type/:name -> MetadataResourceEditPage +metadata/:type/:name/history -> MetadataResourceHistoryPage +``` + +The console's own fragment (`apps/console/src/AppContent.tsx`, abbreviated — read the file +for the full list and its rationale comments): + +``` +system -> SystemHubPage +system/apps | profile | approvals | ai-approvals | audit-log | settings[/:namespace] +developer[/api-console | flow-runs | public-forms | integrations] +docs[/:slug[/:name]] + +# Legacy URLs -> the metadata-admin engine (translate the URL, don't revive the page) +system/objects[/:objectName] -> ObjectRedirect -> …/metadata/object[/:name] +system/metadata[/:type[/:itemName]] -> MetadataRedirect -> …/metadata[/:type[/:name]] + +# Legacy URLs -> framework-owned system objects (objectui#3655) +system/users -> SystemObjectRedirect -> …/sys_user +system/organizations -> SystemObjectRedirect -> …/sys_organization +system/roles -> SystemObjectRedirect -> …/sys_position +system/positions -> SystemObjectRedirect -> …/sys_position +``` + +Four redirects, not five. `system/permissions` is **deliberately absent** as of +`origin/main`: the framework splits what this console called "Permissions" into +`sys_capability` (the definition registry) and `sys_permission_set` (the grant container), +so PR #3673 declined to guess and pinned the unchanged landing in a test instead. The +maintainer has since ruled **A — `sys_permission_set`** on objectui#3655; that redirect is +queued but **not yet on `main`**, so today the URL still falls through to app-shell's tail +route. Check `apps/console/src/AppContent.tsx` before quoting this list. + +Two traps worth internalising, both paid for in objectui#3639 / #3655: + +- The `component/metadata/resource/:name?type=…` spelling is a **legacy alias** whose + element is `LegacyMetadataRedirect` — a second `` onto the `metadata/:type…` + routes above. Never emit it from new code; it buys a redundant hop. It stays declared + only because bookmarks land on it. +- An unmatched URL under `/apps/:appName/*` falls to app-shell's + `:objectName/:maybeRecordId` tail, where `looksLikeRecordId` treats any 6+ character + segment as a record id. A missing route therefore fails in two different ways depending + on word length: short words reach `RouteNotFound`, long ones render `RecordDetailView` + for a nonexistent object. If you add a nav target, declare its route in the same change. ## Key contexts @@ -312,12 +436,32 @@ function HomeLayout({ children }) { ## Common console development patterns -### Adding a new admin page - -1. Create the page component in `pages/system/` -2. Add route in `App.tsx` under the `/system` route group -3. Add card entry in `SystemHubPage.tsx` (icon, label, link) -4. Optionally register as metadata type in registry +### Adding a new admin surface + +**Default answer: don't write a page.** `SystemHubPage` carries an explicit +`@deprecated` docblock — the hand-written card hub is superseded by the metadata-driven +navigation, and it says so in terms worth quoting because they are the platform's +position, not one file's preference: + +> ObjectStack is a metadata-driven platform: every administrable surface (objects, +> metadata types such as `datasource`) is reached through an app's `navigation[]` +> (defined in framework `packages/platform-objects/src/apps/*.app.ts`) and rendered by +> the standard `UnifiedSidebar` → `NavigationRenderer`. New admin surfaces must be added +> as nav items (`type:'object'` or `type:'component'` with +> `componentRef:'metadata:resource'`), NOT as bespoke cards/pages here. + +So, in order of preference: + +1. **A metadata type** → register it in the framework's spec + type registry. The + metadata-admin engine gives you list / create / edit / history for free; add a + `registerMetadataResource()` entry only to override columns or swap in a bespoke editor. +2. **An object** → contribute a `type:'object'` nav item from the owning framework plugin. +3. **A genuinely bespoke React surface** → a `type:'component'` nav item with a + `componentRef` registered in `builtinComponents.tsx`. +4. **Only if none of the above fits**: a page under `apps/console/src/pages/`, its route in + the `systemRoutes` fragment of `apps/console/src/AppContent.tsx`, and a test that + renders the URL. A page in this repo's host is invisible to every other host — see the + reference-assembly note at the top. ### Extending object management @@ -348,10 +492,17 @@ function MyWidget({ schema }) { ## Common mistakes -- Creating standalone pages instead of using MetadataTypeRegistry for admin entities. +- Writing a standalone admin page instead of declaring a nav item / metadata type — see + "Adding a new admin surface". +- Assuming a symbol lives under `apps/console/src/` because this guide's older revisions + said so. Grep first; most of it is in `packages/app-shell`. - Forgetting to set `NavigationContext` in custom layouts — sidebar shows wrong items. -- Not registering custom widgets before MetadataDetailPage renders — SchemaRenderer produces fallback. -- Using legacy routes (`/system/objects`) instead of unified metadata routes (`/system/metadata/object`). +- Not registering custom widgets before the editor renders — `SchemaRenderer` falls back. +- Emitting legacy URLs from new code (`/system/objects`, `component/metadata/resource/…`) + instead of the engine's canonical `…/metadata/:type[/:name]`. The legacy routes stay + declared for bookmarks; that is not a licence to generate them. +- Adding a navigation target without declaring its route in the same change — the tail + route swallows it and fails differently depending on the word's length. - Modifying `src/ui/**/*.tsx` files directly — these are Shadcn upstream files that get overwritten by sync scripts. ## Debugging & Browser Simulation Strategy