Skip to content

feat(app-shell): render a record on a named record page at /apps/APP/OBJECT/record/ID/page/PAGE - #12731

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-12715-named-record-page
Oct 16, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-12715-named-record-page

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #12715
Clause-②: yes

What

A console URL that renders a record on a NAMED record page, so the server's print service can print any record page of an object, not only its default one.

URL shape (the contract objectstack's print service binds to):

/apps/APP/OBJECT/record/ID/page/PAGE

Route :objectName/record/:recordId/page/:pageName, declared beside the record route in packages/app-shell/src/console/AppContent.tsx as RECORD_NAMED_PAGE_ROUTE. The record route /apps/APP/OBJECT/record/ID is unchanged, and it still opens the object's default record page.

Behaviour

  • PAGE renders for record ID only when it is a record page bound to OBJECT (type: 'record', object: OBJECT). A page that declares print renders with its print stylesheet and its print root (data-print-page), and window.__objectui.idle behaves as on the record route. Those are the two signals the print driver's ready predicate reads.
  • Any other name shows the console's existing "Page Not Found" state. That covers an unknown name, a page that is not a record page, and another object's record page. There is never a fallback to the default page and never another page. No record is read and no print root is stamped, so a print of a wrong URL cannot put a wrong document on paper.
  • While the page list loads, only the skeleton is drawn, never the default page.
  • An embedded detail (drawer or split pane) never reads the segment.

How

  • RecordDetailView reads pageName from the route, passes it to usePageAssignment, and checks the named page with isRecordPageOf before it renders. A pending or refused name blocks both the assigned page and the synthesized default.
  • @object-ui/react exports isRecordPageOf(page, objectName), the single reading of "a record page of object O". usePageAssignment's object-based lookup now filters with it, with unchanged behaviour. The hook's published pageName override is NOT narrowed: it still returns any page by that name, and that is pinned. No consumer in the tree relies on it returning a non-record page (measured: the hook's own test only).
  • Why a path segment and not a page query parameter: it matches the record route's /edit sub-route, and the default route's path is untouched. An optional query would let a stray parameter change which document the record route draws. page is also a page-scoped list parameter elsewhere.

Tests

  • RecordDetailView.namedRecordPage-12715.test.tsx mounts both routes from the real route constant. It pins: the happy path (print root present and idle false while the record read is in flight, then true); the unchanged default route; parity on the default route when its default page is a print page; all three refusal cases (no default page, no other page, no print root, no record read); and skeleton-only while pages load.
  • usePageAssignment.isRecordPageOf-12715.test.tsx pins the predicate, the lookup reading through it, and the unchanged pageName override.
  • Each pin was proven red by ablation, with the restored blob equal to HEAD:
    • accept any named page: 2 red
    • ignore the segment: 5 red
    • fall back while pending or refused: 4 red
    • helper ignores the object: 5 red
  • Union on the code head: all RecordDetailView.* tests (63 files, 397 tests); console route, console app and react hook suites (126 files, 1504 tests). All green.

Live reading

Showcase backend from published @objectstack/* 18.0.0-next.2; console from this branch; headless Chromium with print emulation. One throwaway non-default print record page was added in a scratch copy of the showcase only.

  • The named print page has the print root, its letterhead and the record's data, with idle true.
  • Unknown, other-object and home-page names each show Page Not Found, with no print root.
  • The default route is unchanged.

Screenshots and the reading log: branch claude/issue-12715-evidence, folder evidence/12715/.

Acceptance notes

  • The default route still draws the synthesized default page briefly while page metadata loads. This is pre-existing and left unchanged on purpose. That page has no print root.
  • The refusal reuses the existing empty.pageNotFound copy; no locale keys are added.
  • objectstack's print-service.ts default-page rule is widened in objectstack after its pin carries this change. That is not part of this PR.

Provenance: built by the cloud session session_01ACSfW1aH5ptDiXVMpWKPgv (claim 6218257366) and opened by the domain:ui seat 1 PM (session_011K9g23W67dy2zwnyzaoB6o) from that session's final message, because a cloud session cannot open a PR from its container. The seat replaced the dev's own footer lines with this line.


Generated by Claude Code

…/page/PAGE

A new console route, `:objectName/record/:recordId/page/:pageName`, renders
the record on the named page when it is a record page bound to the object,
and shows the page-not-found state for any other name, never the default
page. The record route is unchanged.

`@object-ui/react` exports `isRecordPageOf`, the one reading of "a record
page of object O"; `usePageAssignment`'s object-based lookup filters with it
(behaviour unchanged) and the route asks it before rendering a named page.

Claude-Session: https://claude.ai/code/session_01ACSfW1aH5ptDiXVMpWKPgv
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 73 chunks) 3120.7 KB 3744.1 KB
Main entry chunk (gzip) 66.0 KB 350 KB
Entry file index-D2vSZZr1.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 21.42KB 7.85KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 20.39KB 6.02KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.62KB 0.83KB
auth (authStyles.js) 5.27KB 1.86KB
auth (createAuthClient.js) 41.19KB 11.12KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.26KB 8.03KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (commentAuthorship.js) 1.39KB 0.78KB
collaboration (index.js) 1.93KB 0.85KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 597.81KB 144.28KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 248.96KB 69.45KB
fields (index.js) 290.81KB 74.35KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.67KB 11.85KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 15.35KB 5.51KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 54.94KB 16.05KB
plugin-charts (index.js) 84.97KB 23.36KB
plugin-chatbot (index.js) 201.89KB 48.13KB
plugin-dashboard (index.js) 146.15KB 39.55KB
plugin-designer (index.js) 233.95KB 50.08KB
plugin-detail (index.js) 263.43KB 69.61KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 180.28KB 46.60KB
plugin-gantt (index.js) 178.36KB 45.08KB
plugin-grid (index.js) 270.35KB 74.86KB
plugin-kanban (index.js) 59.42KB 18.48KB
plugin-list (index.js) 132.91KB 34.06KB
plugin-map (index.js) 27.07KB 8.96KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 46.66KB 13.13KB
plugin-timeline (index.js) 39.21KB 11.85KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 93.43KB 23.94KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.23KB 39.46KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.81KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-approval-decision.js) 0.20KB 0.20KB
types (record-components.js) 0.20KB 0.19KB
types (record-panels.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:ui seat 1 · session_011K9g23W67dy2zwnyzaoB6o · 2026-10-16T16:00Z

What was reviewed:

  • Head b7f3ecb914, against objectui#12715 (triage grade 6218185186, p1, the print chain).
  • The claim 6218257366 (Clause-②: yes, a widening) and the dev's report 6219163930 on the card.
  • This is the lane seat's own review at TIER_DEFAULT, the tier a Clause-②: yes declaration alone owes. The published usePageAssignment pageName override is not narrowed (pinned), so no contract-review tier is owed.
  1. Shape.
    • Fixes #12715 is the first line and the only closing keyword. Clause-②: yes starts line 2, and the assignee is os-tesla.
    • 9 files, +548/−6, NOT governed:
      • the route constant and its one route line in AppContent.tsx;
      • RecordDetailView.tsx;
      • isRecordPageOf in usePageAssignment.ts;
      • two pins;
      • two changesets (@object-ui/app-shell, @object-ui/react);
      • two docs pages (print-pages.md, console-architecture.md). These widen the surface on record (the seat's note on 6219163930); they are prose faces that follow the implementation.
  2. The change, read in the diff.
    • RECORD_NAMED_PAGE_ROUTE = ':objectName/record/:recordId/page/:pageName' sits beside the unchanged record route.
    • RecordDetailView resolves the named page through usePageAssignment(objectName, { pageName }). It accepts the page only when isRecordPageOf(named, objectName) (type === 'record' and object === objectName).
    • While the pages are pending it draws only SkeletonDetail. On a refused name it draws the existing empty.pageNotFound state.
    • A pending or refused name blocks both the assigned page and the synthesized default, so there is no fallback and no print root.
    • An embedded detail never reads the segment.
    • The object-based lookup now filters through the same isRecordPageOf, with unchanged behaviour.
  3. The widened surface. One console URL, the contract objectstack's print-service.ts binds to after its pin moves, and one @object-ui/react export, isRecordPageOf. Both are named in their changesets.
  4. Gates (check-runs pinned to this head).
    • 33 success and 3 skipped by design, including Lint, Type Check, Test with all 16 shards, Test (dist pins), Build & E2E, Bundle Analysis, Live E2E (informational) and every changeset check.
    • The four ablations go red (2, 5, 4 and 5), and the restored blobs equal HEAD.
    • The live reading on the published next.2 showcase:
      • the named print page has its print root and idle true;
      • unknown, other-object and home-page names each show Page Not Found with no print root;
      • the default route is unchanged.
  5. Open questions, answered A on the card: the shared export, the path segment, and the one not-found state.
  6. Landing.
    • Ready, then auto-merge into the merge queue.
    • After the merge: verify by content, close the card through Fixes, strip pm:dispatched, and archive cloud session session_01ACSfW1aH5ptDiXVMpWKPgv.
    • objectstack's domain:services seat then widens print-service.ts to this URL once its .objectui-sha carries it.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 16, 2026 16:01
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 16, 2026 16:01
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 16, 2026
Merged via the queue into main with commit c7462e7 Oct 16, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-12715-named-record-page branch October 16, 2026 16:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation package: app-shell package: react tests

Projects

None yet

2 participants