Repository navigation
fix(plugin-grid): the import preview checks a time column and the user import email the way the server does (objectui#11913) - #11973
Conversation
…r import's email as the server does The preview marks a cell exactly where the server's import refuses it, and the import button counts the rows nobody marked (objectui#11814, #11889). Two columns still went unchecked: - A `time` cell: `checkImportCell` had no `time` arm. It now mirrors core's `parseDateCell(cell, 'time')` (`readTimeOfDayCell`, then the year-first form) and refuses with `invalid_time`, in a new localized sentence `grid.import.invalidTime` in every pack. - The user import's email: `identityImportFields` typed it `text`. It is now typed `email` and flagged `emailRule: 'identity'`, and the email arm asks the user import endpoint's rule (`isLikelyEmail` and `isPlaceholderEmail`) instead of the record validator's. Refs objectui#11913. Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
The de pack's new `grid.import.invalidTime` row quotes the cell with one matched pair, so the pinned span and pair counts move from 75 to 76. Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
…eld type `ObjectView` passes `identityImportFields` or `importTargetFields` into the wizard's `fields` through one conditional, which compiles only while this return type is a supertype of `ImportTargetField`. The email entry is typed at its literal against `ImportWizardProps['fields'][number]`, so `emailRule` is still checked where it is written. Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
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
Size Limits
|
|
Standing down on
Generated by Claude Code |
Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
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
Size Limits
|
Contract reviewServed-tier: Rendered 2026-10-08T18:05Z by the at-tier reviewer of Check-runs on the head: 43 completed, 40 success, 3 skipped ( ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each judged against the server it mirrors:
Nothing in the 18 files lies outside the claim plus its amendment, except the one region the dev named, ruled on in ③. ② Semver level
The PR body carries ③ Boundary flags
Acceptance notes and out-of-scope findings, each answered:
One observation of my own, not a flag: better-auth revalidates an address at real-import time beyond Implemented-by: VERDICT: PASS |
Fixes #11913
Clause-②: yes
The Import Wizard's preview marks a cell exactly where the server's import refuses it, because the import button counts the rows nobody marked (objectui#11814, objectui#11889). This PR extends the arms objectui#11889 left in
checkImportCell, without forking them, to cover the two columns the card measured unchecked.Implemented by the
os-devsession dispatched by thedomain:uiseat 3 PM, sessionhttps://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8.What changes
1. A
timecolumn is checked (invalid_time).checkImportCellgains atimearm right after thedate/datetimearm, the order the server'scoerceFieldValueruns them in. The arm asksisImportableTimeCell, a new mirror inimportCoercionContract.tsbesideisImportableDateCell.parseDateCell(cell, 'time')reads it in the published 17.7.0. It asksreadTimeOfDayCellfirst, thenreadYearFirstCellalone, neverreadIsoTemporalCell.readTimeOfDayCellasks core's comparand rule, which takes two forms:ClockTimeValueSchematakes;@objectstack/spec/data(ClockTimeValueSchema), not restated, because the server reads that same schema. The ISO and year-first readings reuse the file's existing private grammar:ISO_TEMPORAL_CELL,YEAR_FIRST_CELL,namesRealCalendarDay,isWallClockandzonedCellUtcYear.grid.import.invalidTime(en:"{{value}}" is not a valid time). The same sentence renders aninvalid_timerow error from the server's dry run (formatDryRunError).record-validator.tsat 17.7.0 judges a writtentimeby the same core rule (isUninterpretableTemporalComparand). So a time the coercion stores is never refused after it.2. The user import's email column is checked by the user import endpoint's rule (
INVALID_EMAIL).identityImportFieldstypes the columnemail, where it wastext, and flags itemailRule: 'identity'.checkImportCell's email arm asksisIdentityEmailfor a flagged field andisRecordEmailfor any other.isIdentityEmailmirrors plugin-auth'sresolveRowIdentityat the@objectstack/plugin-auth@17.7.0tag, which refuses in two cases:isLikelyEmailrefuses the cell. It takes at most 254 characters of printable ASCII with no whitespace, one@that is neither first nor last, and a domain whose last dot is neither its first nor its last character.isPlaceholderEmailtakes it: an address onplaceholder.invalid, in any case.a@b..c, which the record validator refuses. It refuses735431496@柴仟.com, which the record validator takes. A plainemailfield keeps the record rule exactly as before.3. The count. The button counts unmarked rows (objectui#11814), so both new marks move it. Each leg is pinned through the real wizard:
Import 1 Rowon five time rows, and on three identity email rows.Widenings (Clause-②)
@object-ui/plugin-grid(minor).ImportWizardProps['fields'][number]gainsemailRule?: 'identity', which is optional and a single literal. Left out, anemailcolumn keeps the record validator's rule.index.d.tsre-exportsImportWizard, and the typesImportWizardPropsandImportResult. The new mirrors (isImportableTimeCell,isIdentityEmail) and the widenedCellRefusalcode union are module-private, and the packageexportsmap only.and./style.css.dist:emailRule?: 'identity'appears indist/ImportWizard.d.ts, anddist/index.d.tsnames neither new mirror.'record' | 'identity': an explicit'record'would be a second spelling of leaving the key out.@object-ui/i18n(minor). A new key,grid.import.invalidTime, with a{{value}}placeholder, is added besidegrid.import.invalidDatein all ten packs: en, zh, ja, ko, de, es, fr, pt, ru, ar. It is mirrored in the wizard'sIMPORT_DEFAULT_TRANSLATIONS, whichImportWizard.i18n-parity.test.tspins to the en pack.@object-ui/app-shell(patch). No reachable change:identityImportFieldsis not re-exported from the package entry.Changeset:
.changeset/11913-import-time-identity-email.md.File surface
6059931513, plus amendment6060072703, which addsimportCoercionContract.tsand its test.importCoercionContract.ts. It changes in the regions the amendment names: the time reader besideisImportableDateCell,isIdentityEmailbesideisRecordEmail, and the doc comments (isRecordEmail's, which said the user import's column "reaches the wizard typedtext", and the module header). ⚠ One more region, named here so the review can rule on it: the year-first tail of the privateimportDateCellYearmoved into a new privateyearFirstCellYear. The date and time readers now share one year-first reading instead of keeping two copies. The behaviour is unchanged: the existingisImportableDateCellpins pass unmodified.packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts. This is a test beside the packs. Its de census pins (correctly paired spans, and the „ / “ counts) move from 75 to 76, because the new de row adds one matched pair. The history comment is extended in the file's own style.identityImportFields' return type stays the narrow shape. The email entry is typed at its literal againstImportWizardProps['fields'][number], soemailRuleis still checked where it is written.ObjectViewpassesidentityImportFields(...)orimportTargetFields(...)intofieldsthrough one conditional. That expression compiles only because subtype reduction collapses it to the identity shape.importTargetFields'ImportTargetField.options?: unknownis not assignable to the wizard'soptions.ImportWizardProps['fields'], or as aPickthat namesemailRule, turnspnpm --filter @object-ui/app-shell type-checkred at that conditional (TS2322). Both files are outside the surface, so this is recorded under Acceptance notes.Verification, at HEAD
551e5ae8(after mergingmain0bbb67b9)isImportableTimeCellwas compared with the installed@objectstack/core17.7.0's exportedparseDateCell(cell, 'time')over 1592 cells. These are the card's cells, 7 days × 11 clocks × 10 zones × 2 separators, bare days, the year-first forms and assorted prose. The server takes 281 of them, and the two disagree on 0.isIdentityEmailwas compared with the two 17.7.0 functions, restated verbatim from the tag because plugin-auth is not installed here. 19 cells, 0 disagreements.pnpm exec vitest run packages/plugin-grid/ packages/i18n/:Test Files 277 passed (277),Tests 3153 passed | 13 skipped (3166).pnpm exec vitest runon the four app-shell import files (identityImportFields.emailRule-11913,identityImport,identityImportSavedMapping,importTargetFields) plusObjectHooksPanel.newHookTarget-11820(the test objectui#11986 fixed onmain):Test Files 5 passed (5),Tests 31 passed (31).identityImportFieldsor the identity wrapper, or derive the generic target set. CI runs the whole suite.turbo run build --filter='@object-ui/app-shell^...'(28/28):pnpm --filter @object-ui/plugin-grid type-check,pnpm --filter @object-ui/i18n type-checkandpnpm --filter @object-ui/app-shell type-checkall exit 0. Each script name is echoed, and each runs bothtsc --noEmitandtsc -p tsconfig.test.json. The test configs includesrc/**/*.test.tsx, so the new test files are compiled too.pnpm exec eslinton the 17 touched TypeScript files: 0 errors and 11 warnings, all on pre-existing lines this diff does not add.check:control-bytes,check:test-path-roots,check:changeset-claims,check:pending-changeset-literals,check:i18n-keys,check:i18n-drift(1 key(s) addedagainst merge-base0bbb67b9),check:i18n-dead-keys,check:i18n-designer-parity,check:new-line-citations,check:spec-symbols,check:installed-pin-claimsandcheck:phantom-deps.scripts/check-changeset-presence.mjsandscripts/check-changeset-no-major.mjsalso exit 0.check:spec-floorsandcheck:readme-exportsexit 1 withno-artifactfindings only, on packages this branch did not build (app-shellin the floors gate,plugin-gantt,plugin-timeline,plugin-tree,cli). Neither namesplugin-grid, whose builtdistimportsClockTimeValueSchemafrom@objectstack/spec/data. That symbol exists at its^17.5.0floor.check:eager-locale-cataloguesand the eager-closure budget need a console build, so they are left to CI'sBundle Analysis.Reverse checks
These ran at
34ade8bd; the mutated files and the tests they ran are byte-identical at551e5ae8. Each one went throughscripts/ablation-replace.mjsin wrap mode: the anchor hit 1 → 0, the blob changed, and the restore was proven with blob equal to HEAD and an emptygit diff HEAD.timearm deleted fromcheckImportCell.ImportWizard.cellCheck-11913.test.tsxgoes red: 2 failed, 7 passed. Red: "marks what the server refuses with invalid_time" and "marks the time cells the server refuses and counts only the clean row". The10:00pin stays green, as expected with no arm.isRecordEmail(value). The plugin-grid and app-shell11913files go red: 4 failed, 7 passed. Red: the endpoint-rule marks, "not both", and the wizard count in both packages. The first attempt was refused by the tool before it ran anything, because the replacement text was a substring of the anchor. It was re-run with a unique replacement.emailRule: 'identity'flag dropped fromidentityImportFields. The app-shell11913file goes red: 2 failed. Red: the flag pin and the wizard count.The tree was clean (
git statusempty) after each one.Acceptance notes
Not filed, and not fixed here:
ImportTargetField.options?: unknown(importTargetFields.ts) is not assignable toImportWizardProps['fields'][number]['options'].ObjectView's importfieldsconditional compiles only through subtype reduction to the narrower identity shape. It is type-level only: nothing renders differently. The carrier is the next PR that touchesimportTargetFields.tsorObjectView's import wiring; there is no named one today.INVALID_EMAIL,INVALID_PHONE,NO_IDENTITY).formatDryRunErrorhas no case for them, so the endpoint's English text is shown. This is a read, not measured through the public door. Carrier: none.^17.5.0floor.@object-ui/plugin-griddeclares@objectstack/spec^17.5.0, and 17.5.x'sClockTimeValueSchemastill took a zone suffix. 17.6.0 dropped it. A host resolving spec 17.5.x therefore gets 17.5's clock grammar in the preview. This repository's lockfile installs 17.7.0. Carrier: none.Generated by Claude Code