Skip to content

fix(types): ViewNavigationConfig.mode is optional — the second navigation spelling agrees with the spec (#4588) - #4591

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4588-view-navigation-config
Aug 13, 2026
Merged

fix(types): ViewNavigationConfig.mode is optional — the second navigation spelling agrees with the spec (#4588)#4591
yinlianghui merged 1 commit into
mainfrom
claude/issue-4588-view-navigation-config

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #4588

The mismatch, measured

@object-ui/types published two types for one spec object, and they disagreed about whether mode may be omitted.

// packages/types/src/index.ts — the spec's own type, re-exported unchanged
export type { , NavigationConfig,  } from '@objectstack/spec/ui';

// packages/types/src/objectql.ts:1775 (before) — the same six keys, by hand
export interface ViewNavigationConfig {
  /** … @default 'page' */
  mode: 'page' | 'drawer' | 'modal' | 'split' | 'popover' | 'new_window' | 'none';
  view?: string;
  preventNavigation?: boolean;
  openNewTab?: boolean;
  size?: 'auto' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
  width?: string | number;
}

The doc comment claimed @default 'page' on the one key it then made required.

The spec never asked for that. NavigationConfigSchema declares mode: NavigationModeSchema.default('page'), and a .default() lands on the authoring side as | undefined — so the spec publishes its own type as z.input< typeof NavigationConfigSchema >. navigation: { view: 'summary_view' } is legal authored metadata that lets the mode default.

Measured against the published spec build actually resolved here (@objectstack/spec@17.0.0-rc.6, dist/view.zod-CN_gQt7k.d.ts:826), the hand copy had drifted on mode and nothing else — the other five keys carried the spec's exact value domains:

key spec (published .d.ts) hand copy agrees?
mode ZodDefault< ZodEnum > → input-optional required no
view ZodOptional< ZodString > view?: string yes
preventNavigation ZodDefault< ZodBoolean > preventNavigation?: boolean yes
openNewTab ZodDefault< ZodBoolean > openNewTab?: boolean yes
size ZodDefault< ZodEnum >, 6 members same 6 yes
width ZodOptional< ZodUnion > string | number yes

Alias shape: collapse, not restate — the convention, measured

The ruling preferred collapsing to the spec type if the file's conventions allow it. They do, emphatically:

  • objectql.ts:28-30 carries the rule as a banner: "Spec-Canonical Types — imported from @objectstack/spec/ui. Rule: 'Never Redefine Types. ALWAYS import them.'"
  • Nine spec re-exports already live in this file (:51 :57 :68 :74 :80 :86 :134 :143, plus the multi-import at :89-99), and :51 is even a renaming one.
  • No cycle is possible: @object-ui/types has zero workspace dependencies (--filter '@object-ui/types^...' build matched 0 projects); @objectstack/spec is an external dependency the file already imports, and index.ts already re-exports this very symbol from the same entry point.

One correction along the way, worth recording: export type { NavigationConfig as ViewNavigationConfig } from '@objectstack/spec/ui' re-exports but creates no local binding, so the three in-file uses failed with TS2304: Cannot find name 'ViewNavigationConfig'. The file already has the right idiom for that case — the import type { … } from '@objectstack/spec/ui' block at :89 labelled "Import spec types for local use in interfaces below". So:

import type { , NavigationConfig,  } from '@objectstack/spec/ui';

export type ViewNavigationConfig = NavigationConfig;

That is a structural derivation, which is one of the two forms check:spec-symbols sanctions.

Writer / reader census

The three navigation?: ViewNavigationConfig sites (objectql.ts :852 ObjectGridSchema, :1453 ObjectViewSchema, :1674 NamedListView) now accept a mode-less config. Each is pinned in the new suite.

Readers — every .mode read in the repo, and its disposition:

Site Expression Disposition
plugin-view/ObjectView.tsx:542-570 navigationConfig.mode === 'x', behind if (navigationConfig) byte-unchanged (re-verified)
plugin-view/ObjectView.tsx:1174-1177 navigationConfig?.mode === 'x' unchanged
app-shell/views/ObjectView.tsx:1421 authored.mode === 'page' unchanged
react/useNavigationOverlay.ts:224 navigation?.mode ?? 'page' unchanged — already handles undefined
kanban :536 / calendar :396 / gantt :1148 navConfig.mode === 'x' on any-typed values untouched; undefined-mode yields false (non-overlay), agreeing with the 'page' default
plugin-grid/ObjectGrid.tsx:3309 reads the hook result, always defined unaffected

So "fix any reader that inherits the default" is again a measured no-op — no reader of this alias reads mode unguarded. Reported as measurement, not manufactured into edits.

Obsoleted assertions: none, measured. No test in packages/types referenced ViewNavigationConfig at all before this PR (grep over src/__tests__), and navigation-spec-parity.test.ts guards NavigationItemSchema — app navigation, a different object. No assertion existed for this change to obsolete, so none was invented.

A convergence worth naming: app-shell/views/ObjectView.tsx:1414 declares detailNavigation: ViewNavigationConfig and feeds it straight to useNavigationOverlay({ navigation: detailNavigation }) — whose option type #4586 already made mode-optional. The two halves now agree instead of colliding.

Red-first

Predictions were written before the edit; all four materialised. Verbatim tsc -p tsconfig.test.json against the unchanged alias:

src/__tests__/view-navigation-config-spec-parity.test.ts(55,39): error TS2344: Type 'false' does not satisfy the constraint 'true'.
src/__tests__/view-navigation-config-spec-parity.test.ts(74,3): error TS2344: Type 'false' does not satisfy the constraint 'true'.
src/__tests__/view-navigation-config-spec-parity.test.ts(117,11): error TS2741: Property 'mode' is missing in type '{ view: string; }' but required in type 'ViewNavigationConfig'.
src/__tests__/view-navigation-config-spec-parity.test.ts(127,60): error TS2741: Property 'mode' is missing in type '{ view: string; }' but required in type 'ViewNavigationConfig'.
src/__tests__/view-navigation-config-spec-parity.test.ts(128,60): error TS2741: Property 'mode' is missing in type '{ view: string; }' but required in type 'ViewNavigationConfig'.
src/__tests__/view-navigation-config-spec-parity.test.ts(129,58): error TS2741: Property 'mode' is missing in type '{ view: string; }' but required in type 'ViewNavigationConfig'.

6 errors, exit 2 — line 117 is the card's own { view: 'summary_view' }. After the change all three tsc passes are clean.

Two pins did not go red pre-change, and that is the finding rather than a gap: _KeysAreExactlyTheSpecSix and _ModeMembershipIsTheSevenSpecModes already held. The hand copy had the right six keys and the right seven modes — mode's optionality was its only drift. Both pins stay, because they are what catches the next drift.

Type-level pins, per the #4586 idiom

_ModeMembershipIsTheSevenSpecModes compares against NonNullable< ViewNavigationConfig['mode'] >. A bare Equal would now be false for the wrong reason — the | undefined the .default() puts there deliberately, not a change in which modes exist — so NonNullable keeps the assertion pointed at the membership it guards. Same care _ModeIsConfigMode needed one package over.

_IsExactlyTheSpecInput pins the collapse itself: the two names this package publishes for the spec's navigation config are now invariantly one type, so a re-grown hand copy fails the build on the day it drifts.

Reverse verification — direction stated up front

A type-only change, so the honest red is a tsc red, not a suite red. Both were run:

  • Fix removed (git diff > fix.patch + git checkout --, never git stash): tsc -p tsconfig.test.json → the same 6 errors, exit 2.
  • vitest over packages/types packages/plugin-view with the fix removed → 522/522 still green, because the runtime is untouched. Reporting a vitest red here would have been a fabrication.
  • Restore verified byte-exact by sha256sum -c on both files.
  • Non-vacuity of the refusal pins: widening the alias to Record< string, unknown > produced TS2578: Unused '@ts-expect-error' directive. at both refusal sites, plus reds on the collapse, key-set and membership pins. Restored and sha256-verified again.

Verification

  • pnpm --filter '@object-ui/types^...' build0 projects: this package has no workspace dependencies (measured, not assumed).
  • pnpm --filter @object-ui/types type-check — green, all three passes (tsc --noEmit, tsconfig.examples.json, tsconfig.test.json).
  • pnpm exec vitest run --maxWorkers=2 packages/types packages/plugin-view43 files, 522 tests passed (32 types + 11 plugin-view). The new suite is 5 of them, confirmed running in the unit project rather than assumed.
  • Consumer sweep, prefix direction (...@object-ui/types = downstream consumers), after a full 47-project workspace build so no stale artifacts: 40 packages green, including app-shell, plugin-view, plugin-list, plugin-grid, react, apps/console and three examples. The first attempt failed TS2307 on @object-ui/collaboration@object-ui/i18n, which sits outside the filter's scope (42 of 47) — the known stale-artifact trap, fixed by the full build, not by touching anything.
  • ESLint vs origin/main in a comparison worktree, on the edited source file: 15 → 15, NET ZERO, 0 errors both sides. The new test file adds 7 no-unused-vars warnings for its type-level pins — exactly the class the sibling pin file objectql.exportOptions.test.ts already emits (4 of them), which carries no eslint-disable, so this follows the established convention.
  • check:control-bytes, check:phantom-deps, changeset:check, check:spec-symbols — all green. grep -naP control-byte self-scan over both touched files: clean.

Changeset grade: minor for @object-ui/types

Measured from the built .d.ts (clean dist/ + tsconfig.tsbuildinfo between both builds). ViewNavigationConfig occurs in only three consumer-facing positions — the navigation?: properties of ObjectGridSchema, ObjectViewSchema and NamedListView — and in no return position: a regex for ): ViewNavigationConfig / => ViewNavigationConfig over the whole dist matched nothing, and this is a pure type package that publishes no function to hand one back. Every published position is an input position.

So for consumers the change is purely permissive: everything that compiled still compiles, and spec-shaped configs that previously needed an invented mode now compile without one. That gained input shape is a capability rather than an internal repair, which is more than patch describes. The reader-side narrowing is real but secondary, and the in-repo census found no reader affected by it. Never major.

The .d.ts diff is two hunks: the import line, and the interface replaced by the alias. index.d.ts is byte-identical. The only type-level lines in the whole diff are those two.

Surface

packages/types/src/objectql.ts, one new test file in packages/types, one changeset. plugin-view was read-only and is byte-unchanged — the census did not demand an edit there. Untouched as instructed: packages/react (#4585), core/i18n (#4576), ObjectGrid, content/docs/releases/.


Generated by Claude Code

…tion spelling agrees with the spec (#4588)

`@object-ui/types` published two types for one spec object, and they disagreed
about whether `mode` may be omitted. `index.ts` re-exports the spec's
`NavigationConfig` unchanged; `objectql.ts` hand-declared a
`ViewNavigationConfig` over the same six keys with `mode` required — under a doc
comment that itself claimed `@default 'page'`.

The spec declares `mode: NavigationModeSchema.default('page')`, and a
`.default()` lands on the authoring side as `| undefined`, which is why the spec
publishes its own type as the schema's `z.input`. So `{ view: 'summary_view' }`
is legal authored metadata that the hand copy refused, at the three interfaces
spelling `navigation?: ViewNavigationConfig`.

`ViewNavigationConfig` is now that spec type, per this file's own standing rule:
"Never Redefine Types. ALWAYS import them." Measured against the published spec
build, the copy had drifted on `mode` and nothing else. No runtime behaviour
changes — every `.mode` read in the repo is a `=== 'x'` comparison or
`?? 'page'`.

This is #4550 / PR #4586 one package over.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 13, 2026 3:17pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 24.7 KB 350 KB
Entry file index-BCoyAl1j.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.56KB 3.59KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 25.13KB 5.40KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 38.46KB 10.17KB
auth (createAuthenticatedFetch.js) 6.34KB 2.43KB
auth (index.js) 2.35KB 1.07KB
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.02KB 0.88KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 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) 489.33KB 108.47KB
core (index.js) 3.37KB 1.34KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 163.56KB 44.83KB
fields (index.js) 230.37KB 57.17KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 3.35KB 1.38KB
i18n (pickLocalized.js) 3.69KB 1.73KB
i18n (provider.js) 23.12KB 7.62KB
i18n (useDisplayLocale.js) 2.84KB 1.45KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.98KB 10.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.74KB
mobile (index.js) 1.50KB 0.62KB
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) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.86KB 12.91KB
plugin-charts (index.js) 62.10KB 17.67KB
plugin-chatbot (index.js) 181.21KB 43.14KB
plugin-dashboard (index.js) 121.04KB 31.57KB
plugin-designer (index.js) 212.58KB 42.83KB
plugin-detail (index.js) 239.93KB 60.01KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.58KB 27.68KB
plugin-gantt (index.js) 164.30KB 40.02KB
plugin-grid (index.js) 189.37KB 50.33KB
plugin-kanban (index.js) 52.74KB 14.53KB
plugin-list (index.js) 111.13KB 27.12KB
plugin-map (index.js) 18.16KB 5.81KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 41.38KB 11.09KB
plugin-timeline (index.js) 26.68KB 7.66KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.09KB 20.56KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 27.64KB 9.44KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.26KB 0.67KB
react (schema-input.js) 1.45KB 0.83KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.05KB 1.52KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.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 (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
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

@yinlianghui
yinlianghui marked this pull request as ready for review August 13, 2026 15:37
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 13, 2026
Merged via the queue into main with commit ab04728 Aug 13, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4588-view-navigation-config branch August 13, 2026 15:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(types): @object-ui/types publishes two spellings of the spec's navigation config, and the hand-written ViewNavigationConfig still requires mode

2 participants