Skip to content

fix(plugin-grid): a fixed-currency field shows its own currency in the grid cell and the summary footer (#10354) - #10418

Merged
os-litant merged 6 commits into
mainfrom
claude/issue-10354-grid-fixed-currency
Sep 25, 2026
Merged

os-litant merged 6 commits into
mainfrom
claude/issue-10354-grid-fixed-currency

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #10354

A fixed-currency field (currencyConfig: { currencyMode: 'fixed', defaultCurrency: 'JPY' }) now shows its own currency in the grid cell and in the column-summary footer. Before this change, both faces showed the tenant's currency: a JPY-fixed 1234 in a USD tenant read $1,234.

What changed, in the triage comment's order

  1. The release-bound step first. .changeset/9269-grid-summary-percent-converged.md said "decimals still reads column.precision, unchanged". That has been false since objectui#9295. The sentence now names scale. Only the body changed; the frontmatter bytes match the base (compared with od -c).
  2. Both currency halves together.
    • ObjectGrid.tsx: each of the three configured-column cell bags (ListColumn objects, a string array, inline rows with a fields projection) now copies the field's currencyConfig as declared.
    • useColumnSummary.ts: the column hints now pass the field's currencyConfig to resolveFieldCurrency. The hook's fieldMetadata parameter type gains currencyConfig?: CurrencyConfig, using the spec's own type from @objectstack/spec/data.
  3. Rider. In packages/plugin-grid/README.md, the percent-footer sentence now says the width comes from scale, not precision.
  4. Pins. A new test file, packages/plugin-grid/src/__tests__/gridFixedCurrency-10354.test.tsx, has 15 cases (see below).
  5. The sibling cards objectui#10355 and objectui#10356 are not touched, and none of their files are in this diff.

Mechanism assumptions, measured

  • A1 (the key): pass currencyConfig through verbatim. resolveFieldCurrency in @object-ui/i18n checks field.currency first, then field.currencyConfig?.defaultCurrency, then a legacy defaultCurrency, and finally the tenant default. It does not read currencyMode. So the grid hands over the whole object and adds no mode rule of its own: whatever the resolver decides a mode means, the grid now agrees. One consequence: a dynamic-mode field that has a currencyConfig now also shows defaultCurrency on these two faces. The auto-generated columns, the detail view and the metric tile already did. See Acceptance notes.
  • The footer reads currencyConfig from the FIELD only. ListColumnSchema (spec 17.4.0) declares no currencyConfig, so reading it off the column would create a second, undeclared spelling (Commandment #0.1).
  • A2 (all three sites): holds, exactly three. I located the sites by content (fieldMeta.currency = ). Two other cell paths exist: the auto-generated columns and the record-detail panel. Both hand the renderer the whole field def and build no bag, so they need no change.
  • A3 (serial constraint): holds. The hunks stay on the three cell-bag sites, the hook's type and hints, and the one README sentence. origin/main (read at 2fc2a24) has not changed any touched file since the base 721d1e0, so no merge was needed.

Pins and ablation

Fixture: a USD tenant. amount is fixed to JPY (1234). dyn is dynamic EUR (3456) and appears only in the agreement cases. plain is a currency field with no currencyConfig (5678) and serves as the control.

case base tree this branch
JPY-fixed cell, each of the three paths RED GREEN
cell agrees with the real CurrencyCellRenderer given the whole def, three paths RED GREEN
JPY-fixed footer through the grid; footer agreement; footer through the public hook RED GREEN
controls: three cells, the grid footer, the hook GREEN by design GREEN
reference: the auto-generated columns GREEN by design GREEN

Each ablation ran on committed code. A trap restored the files afterwards, and each restore was proven by an empty git diff HEAD plus a blob hash equal to HEAD's.

  • Both source files reverted to the base, at 975365b: Tests 9 failed | 6 passed (15). A typical failure is expected 'Yen Amount: Sum: $1,234' to be 'Yen Amount: Sum: ¥1,234', and the grid's cells read ["1Open","Tower","$1,234","$3,456","$5,678"].
  • Only ObjectGrid.tsx reverted (at cd2b7d8, which had 14 cases): 6 failed, all of them cell cases; the footer cases stayed green.
  • Only useColumnSummary.ts reverted (same head): 3 failed, all of them footer cases; the cell cases stayed green.
  • Only the ListColumn site deleted, via ablation-replace.mjs: 2 failed, both of them 'ListColumn objects' cell cases.

The two half-reverts show that each half has its own pins: reverting either one splits the cell from the footer again.

A reverse check on the type ran at 975365b with tsc -p tsconfig.test.json:

  • writing currencyMode: 'fixd' in the typed hook fixture fails with TS2820;
  • removing the new member from the hook's type fails with TS2353 on the fixture;
  • the restored tree exits 0.

Local verification (at 975365b unless noted)

  • plugin-grid, the whole package: 151 test files run in three parts (49, 50 and 52 files), 422 + 411 + 630 = 1463 tests passed.
  • Outside plugin-grid: 19 test files that name ObjectGrid.tsx, useColumnSummary or the plugin-grid README, plus machineLocaleCensus-9909. All 1193 tests passed.
  • Type-check: after the closure build (turbo run build --filter='@object-ui/plugin-grid^...', 13 tasks), pnpm --filter @object-ui/plugin-grid type-check passed. tsc -p tsconfig.test.json --listFilesOnly lists the new test file.
  • Gates that exited 0:
    • Changesets: check-changeset-presence, check-changeset-no-major, check-changeset-overwrite (report-only; it reports the intended 9269 body edit), check-changeset-fixed, check:changeset-claims (7 pending changesets name a touched file; I read each one, and none is made false).
    • Citations and bytes: check:new-line-citations (0 new), check:control-bytes.
    • Imports and specs: check:spec-symbols, check:phantom-deps, check:esm-specifiers, check:self-import, check:designer-field-key-parity.
    • Others: check:doc-fences, check:handler-key-reads, check:test-path-roots, check:unreferenced-sources, check:vi-mock-specifiers, check:metadata-write-doors, check:comment-mask-corpus.
  • check:spec-floors: plugin-grid measured, 10 other packages not. After building plugin-grid, the gate reports no plugin-grid finding. The new CurrencyConfig import is exported from ./data at the declared ^17.0.0 floor, which I also confirmed in the 17.0.0 tarball. The gate still exits 1 because 10 other packages have no build output here (no-artifact). Those 10 are NOT MEASURED, reason: they need a whole-tree build, which CI does.
  • check:readme-exports: NOT MEASURED, reason: 23 of 40 packages are unbuilt, so the gate's population floor was not met. The README hunk is prose, with no fenced block.
  • Lint, a narrowed run. I ran eslint with --format json on the 3 touched source and test files: 3 files, 0 errors.
    • Warnings: ObjectGrid.tsx has 208 and useColumnSummary.ts has 6, the same counts as on the base. The new test has 2 no-explicit-any warnings.
    • Population: packages/plugin-grid is linted with the root eslint.config.js.
    • Why the narrowing excludes nothing: that config sets no parserOptions.project or projectService, so linting is not type-aware and this diff cannot change the verdict on an untouched file. The full pnpm lint run belongs to CI.

Acceptance notes

  • Dynamic mode: measured here, not changed. resolveFieldCurrency reads currencyConfig.defaultCurrency in every mode, and no objectui source reads currencyMode.
    • The spec describes dynamic as "user selectable". Its field-key guidance says "A field without one uses the tenant default at runtime", where "one" means a fixed currency.
    • The resolver's own doc comment says the key is read in "(fixed mode)".
    • This PR passes the key through verbatim (A1), so for a dynamic field the grid now shows the same currency as the tile and the detail view.
    • I reported this in the dev report as a finding for the seat. The resolver is outside this card's file surface.
  • Field-level currency. The cell bag still copies a field-level currency, which the spec refuses by name. I left it unchanged because it is out of scope.
  • The 9269 changeset's signature sentence stays. It says "No exported signature changes: useColumnSummary's parameters and return shape are untouched". That sentence describes the 9269 change and is still true of it. This PR's own changeset states the parameter type change this PR makes.

Changeset

.changeset/10354-grid-fixed-currency-cell-footer.md: a patch for @object-ui/plugin-grid.

Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C


Generated by Claude Code

…e release

The pending changeset said `decimals` still reads `column.precision`. That has
been false since objectui#9295, which moved the percent footer's width to
`scale`. Body only; the frontmatter is byte-identical.

Part of objectui#10354.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
…s currencyConfig

`@objectstack/spec` refuses a field-level `currency`, so a fixed currency is
declared only as `currencyConfig: { currencyMode: 'fixed', defaultCurrency }`.
`resolveFieldCurrency` reads it, but the grid never handed it that key: the
three configured-column cell bags copied `currency` / `precision` / `scale`
only, and the footer's column hints carried no `currencyConfig`. A JPY-fixed
amount in a USD tenant therefore read `$1,234` in the cell and the footer,
while the metric tile and every whole-def cell read `¥1,234`.

Both halves move together: each cell bag copies `currencyConfig` verbatim, and
`useColumnSummary` hands the field's `currencyConfig` to the same resolver, so
the cell and the footer cannot split again.

The README's percent-footer sentence now names `scale`, the width it has read
since objectui#9295.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
…e column paths

A USD tenant with a JPY-fixed field: the cell on each configured-column path
and the summary footer (through the grid and through the public hook) read
`¥1,234`, while a field with no `currencyConfig` still reads the tenant `$`.
An agreement table holds the grid to the real `CurrencyCellRenderer` given the
whole field def, so it restates no resolution rule.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
The controls are GREEN on the base tree by design, so each one now reads as
its own case instead of trailing a fixed-currency assertion that fails first.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
…ace; add the changeset

The auto-generated columns hand the cell the whole field def and build no bag,
so they already read the field's own currency. The new case is GREEN on the
base tree by design and names the face the configured paths now match.

The changeset is a `patch` for `@object-ui/plugin-grid`, with the dynamic-mode
consequence stated.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
… spec's CurrencyConfig

A restated `{ defaultCurrency?: string }` refused a field def written in the
spec's own shape: an object literal carrying `currencyMode` failed the excess
property check. The member is now `CurrencyConfig` from `@objectstack/spec/data`,
and the hook's pin passes a typed literal with no cast so `type-check` reads it.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
@github-actions github-actions Bot added documentation Improvements or additions to documentation plugin tests labels Sep 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 7 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/4730-retire-dead-locale-key-batch.md

  • names ObjectGrid.tsx → packages/plugin-grid/src/ObjectGrid.tsx — edited by this change

    • Superseded twin vocabularies. cellRender.* and rowAction.* duplicated a grid.* vocabulary that won. RowActionMenu.tsx is fully i18n-wired and reads grid.openMenu / grid.edit / grid.delete; ObjectGrid.tsx reads grid.empty / grid.yes / grid.no / grid.systemFields. The twins had no reader on either side. - Labels that outlived their control. calendar.agenda labelled a view mode objectui#5740 retired from CalendarViewMode (now 'month' | 'week' | 'day'). home.quickActions.createApp*, layout.systemNav.createApp, actionDialog.defaultActionTitle / .ok and grid.bulk.selectPlaceholder sit in namespaces whose consumers are live and wired but demonstrably read other siblings. - Surfaces that left the product. map.* is the strongest form: @object-ui/plugin-map declares no @object-ui/i18n dependency and contains no t() call at all, so it cannot consume a locale string. home.stats.* and recordDetail.viewersTooltip name surfaces nothing renders.

.changeset/6597-retire-fieldmeta-referenceto.md

  • names plugin-grid/src/ObjectGrid.tsx → packages/plugin-grid/src/ObjectGrid.tsx — edited by this change

    No authoring story survived the search either. ObjectGrid's own relational-meta pass-through (applyRelationalMeta, plugin-grid/src/ObjectGrid.tsx) copies reference_to / reference / display_field / etc. from the SCHEMA field def only, at all three of its call sites — never from an authored column override. No doc, example, or fixture in this repo shows a table column pinning a lookup's target away from what its schema field already says. Under the maintainer's standing startup-stage rule (2026-08-27: deprecated/alias spellings retire immediately, no transition windows), no measured demand selects withdraw.

.changeset/6650-listcolumn-wrap-implemented.md

  • names packages/plugin-grid/README.md → packages/plugin-grid/README.md — edited by this change

    @objectstack/spec declares ListColumn.wrap and describes it to authors as "Allow text wrapping", and packages/plugin-grid/README.md shows it in its authored-column example. No renderer anywhere implemented it. Long cell text stayed clipped to one line, with no error, no warning and no feedback of any kind — a promise made at authoring time and silently broken at render time.

.changeset/7762-object-grid-export-options-bare-array-refusal.md

  • names ObjectGrid.tsx → packages/plugin-grid/src/ObjectGrid.tsx — edited by this change

    What was measured, on this branch's base. The mirror declared NO exportOptions member at all, and BaseSchema is .passthrough(), so ObjectGridSchema.safeParse({ type: 'object-grid', objectName: 'accounts', exportOptions: ['csv', 'xlsx'] }) returned success: true with the array back VERBATIM — as did { formats: ['csv', 'pdf'], compression: 'gzip' }. Nothing on the render path parses, and ObjectGrid.tsx reads schema.exportOptions?.formats and only that, so the authored array then lost SILENTLY to the ['csv', 'json'] default: the useEffect that warns about dropped formats reads .formats too and returns early when it is absent, while !!schema.exportOptions kept the export button on screen. An author declared ['csv', 'xlsx'] and got csv/json with no error, no warning and no console line. The two authoring faces disagreed in the direction opposite to objectui#6956's: the TypeScript interface already declared the object form only, so TS refused what zod admitted.

.changeset/9722-bulk-executor-datasource-face.md

  • names ObjectGrid.tsx → packages/plugin-grid/src/ObjectGrid.tsx — edited by this change

    • ObjectGrid.tsx → RecordDetailPanel: removing it type-checks GREEN. It was paying for nothing — both sides already declare the same DataSource | undefined from @object-ui/types. Removed outright. - ObjectGrid.tsx → BulkActionDialog: removing it reddens, and it was paying for TWO things at once — the optional-vs-required arm (the grid declares dataSource?: DataSource, the dialog demands one) AND the bulk-door contravariance. Only the first still needs erasing, so it is now a non-null assertion: today's runtime behaviour is preserved exactly, and any future drift of the face reddens at that site instead of passing silently.

.changeset/9853-grid-non-positive-page-size.md

  • names ObjectGrid.tsx → packages/plugin-grid/src/ObjectGrid.tsx — edited by this change

    Refuse a non-positive pageSize at all three of ObjectGrid.tsx's read points, instead of giving two different answers for one authored value (objectui#9853).

.changeset/grid-summary-footer-tenant-locale-9294.md

  • names packages/plugin-grid/src/useColumnSummary.ts → packages/plugin-grid/src/useColumnSummary.ts — edited by this change

    formatSummaryLabel in packages/plugin-grid/src/useColumnSummary.ts handed Intl no locale argument at seven call sites — the currency arm (both branches plus its constructor-threw fallback), the average arm, the plain numeric default, the percent-of-rows arm and the count/non-numeric arm. An omitted or undefined locale is not "no opinion": it is the machine's locale, which is neither of this renderer's two channels, and it is the one thing useDisplayLocale's own doc comment tells callers not to do. All seven now take the concrete BCP-47 tag that hook already resolves (tenant locale, then UI language, then 'en') — the same tag the cells directly above the footer are formatted with, and the same one the percent column took in objectui#9269.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with 2fc2a2439 (merge-base with origin/main): 4 file(s) changed outside .changeset/, read against 1367 pending declaration(s) that publish a body (1941 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3043.7 KB 3104.5 KB
Main entry chunk (gzip) 148.3 KB 350 KB
Entry file index-Bv5b01Io.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) 16.68KB 6.20KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
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.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
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) 26.08KB 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.68KB 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) 542.47KB 129.64KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 224.12KB 62.26KB
fields (index.js) 255.56KB 64.73KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
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) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
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.01KB 3.93KB
plugin-calendar (index.js) 51.44KB 14.62KB
plugin-charts (index.js) 72.54KB 20.26KB
plugin-chatbot (index.js) 198.27KB 47.18KB
plugin-dashboard (index.js) 133.45KB 35.33KB
plugin-designer (index.js) 216.12KB 44.37KB
plugin-detail (index.js) 260.94KB 67.93KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 146.34KB 37.47KB
plugin-gantt (index.js) 168.47KB 41.53KB
plugin-grid (index.js) 215.43KB 58.90KB
plugin-kanban (index.js) 49.30KB 15.39KB
plugin-list (index.js) 114.36KB 28.24KB
plugin-map (index.js) 22.05KB 7.14KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.72KB 8.96KB
plugin-tree (index.js) 10.74KB 3.76KB
plugin-view (index.js) 85.86KB 21.38KB
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.66KB 3.50KB
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) 114.58KB 37.60KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
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) 0.20KB 0.18KB
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) 4.74KB 2.25KB
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 (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 (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 975365bf3da77938f75cf2af9824db12f00c5bcb

Rendered by an isolated review subagent spawned by the domain:ui#4 seat; its served tier was checked against its transcript stamps (87 of 87 model stamps at the review tier). Adopted by this seat.

① Derived judgments

  • (a) Completeness of the two halves: holds. At head, packages/plugin-grid/src has exactly three const fieldMeta: Record ... builders, all in ObjectGrid.tsx, and each is one of the three configured-column branches: the ListColumn-objects branch (typeof cols[0] === 'object', bag keyed on col.field), the string-array branch ((cols as string[]).filter(...)) and the inline-rows branch (fieldsToShow.map(...) over schemaFields || Object.keys(inlineData[0])). All three now copy objectDefField.currencyConfig / fieldDef.currencyConfig verbatim, and the only consumer of each bag is the cell renderer (field={fieldMeta as any}). Every other path hands over the whole def and builds no bag: the auto-generated columns (fieldForCell = translatedField, the def with translated options), the record-detail panel (RecordDetailPanel receives objectSchema whole; plugin-detail's DetailView calls resolveFieldCurrency({ ...objField, ...sectionField })), inline edit (renderCellEditor builds { name, ...fieldDef } for FieldEditWidget, so CurrencyField's resolver call already sees currencyConfig), and the mobile renderFieldValue (Renderer gets fieldDef). Grouped rows (useGroupedData.ts, GroupRow.tsx) and the export path do no currency formatting at all, so there is nothing for them to carry. The footer: useColumnSummary(summaryColumns, data, objectSchema?.fields) receives the whole fields map, and columnHints now carries currencyConfig: meta?.currencyConfig into the same resolveFieldCurrency. Residual, pre-existing and outside the card's two named sites: the mobile card's one-line amount summary (formatCompactCurrency(row[amountCol.accessorKey], resolveFieldCurrency(amountCol as any, tenantCurrency), displayLocale)) resolves from a column DRAFT (accessorKey, header, type, cell, width, pinned, wrap; no currency, no currencyConfig), chosen by a name heuristic (classify(accessorKey) === 'amount'), so a JPY-fixed amount on that card line still reads the tenant $. It is not a fieldMeta bag and not a regression of this PR; it belongs on a follow-up card in the same lane. Nothing on the card's surface still drops currencyConfig.

  • (b) Regression risk from the currencyMode gap: conclusion (ii), a pre-existing resolver defect the grid now agrees with, correctly carried out of scope. Not a FAIL. Readings: (1) The resolver at head (packages/i18n/src/currency.ts) is field?.currency || field?.currencyConfig?.defaultCurrency || field?.defaultCurrency || tenantDefault || undefined; it never reads currencyMode, and its parameter type declares only currencyConfig?: { defaultCurrency?: string }. (2) objectstack origin/main (7e6ca1787): Field.currency: (config: FieldInput = {}) then ({ type: 'currency', ...config }) injects nothing; FieldSchema.currencyConfig is CurrencyConfigSchema.optional() with no field-level default, so a field that did not author currencyConfig never gains one, parsed or not; CurrencyConfigSchema's defaults (currencyMode 'dynamic', defaultCurrency 'CNY', precision overwritten to 2) materialize only for an AUTHORED, possibly partial, config. The registry (packages/objectql/src/registry.ts) calls this.validate(type, item) and discards the result, then collection.set(storageKey, item) stores the authored item; grep over packages, apps, examples finds no builder, service or serving path that EMITS currencyConfig (driver-sql's collision table and service-analytics only read it). (3) Authorship: objectstack examples author currencyConfig in exactly three places (app-showcase account, field-zoo, semantic-zoo), all currencyMode: 'fixed' with defaultCurrency: 'USD'; zero currencyMode: 'dynamic' and zero partial configs in non-test source; packages/platform-objects and the apps author none; objectui non-test source authors zero currencyConfig and zero dynamic. (4) In objectui, CurrencyCellRenderer (resolveFieldCurrency(field as any, tenantCurrency) with the whole def), CurrencyField.tsx, ObjectMetricWidget.tsx (valueFieldDef), DetailView.tsx, ObjectGantt.tsx (def) and the grid's own auto-generated columns ALREADY show currencyConfig.defaultCurrency in every mode; the pin's dyn agreement rows hold the grid to the real cell renderer given the whole def. So a dynamic-mode field with a defaultCurrency is reachable only by an author who writes it, is shipped by no example, and already reads that currency on every other face; the grid was the odd one out, not the first face.

  • (c) The spec type import: sound. import type { CurrencyConfig } from '@objectstack/spec/data' is type-only and erased at emit. @objectstack/spec is a runtime dependencies entry of @object-ui/plugin-grid at ^17.0.0 (and a devDependency at the same floor). The published 17.0.0 tarball (spec-17.0.0.tgz, read into scratch) exports ./data with types: ./dist/data/index.d.mts (and .d.ts for require), and that file re-exports CurrencyConfig, CurrencyConfigParsed and CurrencyConfigSchema; at objectstack origin/main (spec 17.4.0) it is export type CurrencyConfig = z.input of CurrencyConfigSchema in field.zod.ts. useColumnSummary is a public export (index.tsx: export { useColumnSummary } from './useColumnSummary'), so the emitted declaration of its fieldMetadata parameter will reference @objectstack/spec/data (external per the package's vite-plugin-dts config). That is not a new consumer obligation: the same module already exports ColumnSummaryType = ColumnSummary from @objectstack/spec/ui, and the package's other sources import @objectstack/spec/data and /security; the spec being a dependency rather than a peer, resolution is transitive.

  • (d) The pin is real and can fail. It renders the real ObjectGrid, the real useColumnSummary (via renderHook) and the real CurrencyCellRenderer under I18nProvider and a USD LocalizationProvider; the only vi.fn uses are scrollIntoView and the data source's find; there is no vi.mock of @object-ui/i18n or the resolver. renderGrid waits for the enriched header (Yen Amount) so a pre-schema $ cannot pass. The unfixed control plain (no currencyConfig) is asserted at $5,678 on all three paths, on the grid footer and on the hook. Arithmetic: at head 3 it.each(PATHS) × 3 paths = 9 plus 6 it( = 15. Base RED = fixed cell ×3 + agreement cell ×3 + grid footer fixed + grid footer agreement + hook fixed = 9; base GREEN = control cell ×3 + grid footer control + hook control (5 controls) + the auto-generated reference = 6. At cd2b7d873 the file has 9 + 5 = 14 (the auto-generated reference landed in a38062d83); cell-only revert = the 3 fixed + 3 agreement cell cases = 6, footers green through the untouched hook; hook-only revert = grid footer fixed + grid footer agreement + hook fixed = 3; ListColumn-site deletion = that path's fixed-cell and agreement case = 2 (its footer cases go through the hook, so stay green). All four reported counts are consistent with the case list.

  • (e) Every changed or added sentence is true. 9269 body: "This change leaves decimals alone. Since objectui#9295 it reads the column's scale (zero decimal places when absent), and precision is not read." matches the head percent arm const decimals = column?.scale ?? 0; formatted = formatPercent(value, decimals, displayLocale); with no precision read (the hint's scale is col.scale ?? meta.scale). Frontmatter bytes are identical at base and head (od -c of the first six lines, '@object-ui/plugin-grid': minor). README: "The width comes from the column's scale (zero decimal places when absent), never its precision (objectui#9295)." — same code. The whole of 10354-grid-fixed-currency-cell-footer.md checks out: the spec refuses a field-level currency (FieldSchema guidance: "currency is not a field key — a fixed currency is declared as currencyConfig: ..."); the three named paths are the three branches; the resolver reads currencyConfig.defaultCurrency; the hook's fieldMetadata gains currencyConfig?: CurrencyConfig; ListColumnSchema (view.zod.ts, strictObject) declares no currencyConfig (0 hits in packages/spec/src/ui); the auto-generated columns already showed the field's currency; the dynamic-mode consequence is stated and true. The retained 9269 sentence "No exported signature changes: useColumnSummary's parameters and return shape are untouched" is scoped by its own entry to the 9269 change and remains true of that change; published beside the 10354 entry, which states the added optional member itself, a reader gets both facts in order. Not false; no fault.

  • (f) The out-of-scope finding is real, correctly owned, and not yet carded. The resolver source confirms no currencyMode read; git grep currencyMode over objectui non-test source returns only this PR's two comments. Right owner: @object-ui/i18n resolveFieldCurrency (packages/i18n/src/currency.ts), whose own doc comment says the key is read "(fixed mode)", against the spec's CurrencyConfigSchema description "dynamic (user selectable) or fixed (single currency)"; every consumer (CurrencyCellRenderer, CurrencyField, ObjectMetricWidget, DetailView, ObjectGantt, useColumnSummary, now the grid bags) moves with it. Coverage: the search API is unavailable to this session (repo-scoped endpoints only), so all 240 open objectui issues were walked through the repo endpoint and title plus body grepped for currencyMode, resolveFieldCurrency and dynamic-near-currency; the only hit is finding(plugin-grid): a fixed-currency field (currencyConfig.defaultCurrency) shows the tenant's currency on the grid cell and the summary footer, because the column cell bag and columnHints drop currencyConfig #10354 itself. The seat should file it with the dev's dedupe words.

② Semver level

.changeset/10354-grid-fixed-currency-cell-footer.md declares '@object-ui/plugin-grid': patch. Right. AGENTS.md (版本号策略): "minor/patch 独立演进——objectstack 没动时不必跟发;objectui 自己的改动照常用 changeset 推进" and "推论:changeset 里不要声明 major —— fixed 组任一 major 都会把全组推上去 … objectui 自身的破坏性变更也标 minor(在正文里写清 breaking 语义即可)". The change is a bug fix with an additive optional type member; nothing exported is removed or renamed; not major (check-changeset-no-major green in CI). The presence rule (line 167: any src/ change in a released package needs a changeset) is met; @object-ui/plugin-grid is in the fixed group. The dynamic-mode widening is a consequence of the fix on faces that already behave that way and is stated in the body, so patch rather than minor is defensible. No other package needs a changeset: only packages/plugin-grid sources changed; @object-ui/i18n is untouched.

③ Boundary flags

Implemented-by: claude/issue-10354-grid-fixed-currency
Reviewed-by: session_01BP8CMtACxTdLjqR6rhd33C

VERDICT: PASS


Generated by Claude Code

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 plugin tests

Projects

None yet

2 participants