Repository navigation
fix(app-shell): a report's or dataset's Label with no machine name leaves the Name empty (objectui#12789) - #12806
Merged
objectstack-fleet[bot] merged 1 commit intoOct 16, 2026
Conversation
…aves the Name empty (objectui#12789) The report and dataset designers derived the Name from the Label with `toFieldName`, whose empty result is the placeholder `field`. A Label in a non-Latin script (`收入`), or a cleared Label, therefore named the item `field` silently. Both now derive with `toFieldNameOrEmpty`, the same strict rule with no placeholder, beside `toFieldName` in `previews/object-fields-io.ts`; the Name stays empty, as the object designer's does, and the save path asks for one. `nameFollowsLabel` compares against the same function. `toFieldName` keeps its `field` fallback for its other callers. Claude-Session: https://claude.ai/code/session_01Vk15bz8xHtKGUfXfCm16h4 Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ 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
|
This was referenced Oct 16, 2026
Contributor
Author
席位核对 — ACCEPT ·
|
| case | before (8df2608) |
this PR (e4b532b) |
|---|---|---|
| report / dataset, Label 收入 | Name field |
Name empty |
| report / dataset, Label cleared | Name field |
Name empty |
| a Latin Label typed later | revenue_board / revenue_board_q |
the same, unchanged |
| object designer (control) | empty | empty |
- Save on an empty Name sends no request. The report shows "A name is required." with the client spec banner, the dataset shows "A name is required.", and the object designer behaves the same.
- On
main, by contrast, the dataset saved silently asfield:PUT /api/v1/meta/dataset/fieldanswered 200, "Saved dataset 'field'".
3. Gates (check-runs pinned to this head).
- 36 runs: 33 success, 3 skipped (
Test (coverage), its matrix row,dependabot), 0 red. - Among the successes:
Lint,Type Check,Repo Gates,Test,Test (shard 1/16)…(16/16),Test (dist pins),Build & E2E,Build Docs,Bundle Analysis(PASS,6225575516: eager closure 3129.9 KB of 3744.1 KB),Live E2E (informational), the changeset checks,Line Citation GateandGoverned Surface Queue Guard. - The dev's local subset:
- 23 files / 235 tests, covering every report, dataset and object inspector suite, every
object-fields-iotest, the objectui#12680 host test and bothCreateItemDialogtests; - the closure build and the app-shell type-check;
- eslint, with 0 errors;
- changeset presence and new-line citations.
- 23 files / 235 tests, covering every report, dataset and object inspector suite, every
- Red on
main's inspectors: 8 failed / 2 passed. - Ablations, each restored and proven: A1 6 red, A2 2 red, A3a 2 red, A5 9 red. A3 and A4 stay green, and the PR explains why:
- A3, the loose rule, differs only on trailing punctuation, which objectui#12746's pin covers in the same run;
- A4's arm is redundant with the OrEmpty comparison.
4. Accepted deviations.
toFieldName's body becomestoFieldNameOrEmpty(raw) || 'field'. The claim held the'field'fallback unchanged, and it is: the result is identical for every input, and its existing pin (''and 报修工单 both givefield) is green. Delegating keeps the strict rule in one spelling.CreateItemDialogrelies on that fallback as a sentinel and is untouched.genGroupKeynever reaches the empty case.
- Housekeeping, cleaned up. The dev wrote one push log beside its worktree, outside the scratchpad, and created a temporary ref in the shared
.git. Both were deleted at once, and the final tree had 0 changed paths. Noted, not repeated: the next dispatch says never to create refs in the shared.git. - Prose checked against the diff:
- the changeset's "Such a Label now leaves the Name empty, as the object designer already does, and saving asks you for a name";
- "A Label that yields a name derives exactly what it derived before";
- the
toFieldNameOrEmptydocblock.
5. Out of scope (where each went).
- A create flow saves the spec seed's placeholder binding. A dataset with no base object saves
object: example_object, and its reads are refused (measured). It is the second occurrence of this card's class. → filed objectui#12810, the family card, with an enumeration pin overBUILTIN_METADATA_CREATE_SEEDS. CreateItemDialog's'field'sentinel also refuses an input whose real identifier isfield. Read from source only. → Acceptance notes.
6. Landing.
- Not governed:
check-governed-queue-guard.mjs --testover the 7 paths reads NOT GOVERNED. - 303 lines changed, under the 3000-line threshold.
Clause-②: no. Every check is green or an expected skip.- ⇒ The seat lands it: relay
pr_ready+automerge_enable, through the merge queue. - On MERGED,
Fixescloses objectui#12789.
Generated by Claude Code
objectstack-fleet
Bot
deleted the
claude/issue-12789-label-derives-empty-name
branch
October 16, 2026 22:28
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #12789
Clause-②: no (designer only; no export, prop or type changes)
What changes
In create mode, the report and dataset designers derive the Name from the Label until the author types a Name. They derived it with
toFieldName, which returns the placeholderfieldwhen nothing is left. So a Label in a non-Latin script (收入), or a cleared Label, named the report or datasetfieldwithout telling the author. The dataset then saved asfield(reading below), and a second such item would collide with it.Both Label handlers now derive with
toFieldNameOrEmpty, a new function besidetoFieldNameinpreviews/object-fields-io.ts. It applies the same strict rule, but returns''wheretoFieldNamereturnsfield. The Name stays empty, as it does in the object designer (whoseslugifyreturns''), and the save path asks the author for a name.nameFollowsLabelcompares the stored Name with the same function. So:main, a Name typed asfieldnext to a non-Latin Label was overwritten by the next Label edit.A Label that yields a machine name derives exactly what it derived before: the strict form, no trailing
_, thef_prefix for a leading digit, andfieldfrom the LabelField.toFieldNamekeeps itsfieldfallback. Its body is nowtoFieldNameOrEmpty(raw) || 'field', which returns the same result for every input. Its existing pin inobject-fields-io.test.tsis unchanged and green:toFieldName('')andtoFieldName('报修工单')both returnfield. Its other callers:CreateItemDialog(studio-design) relies on the fallback: it treats afinalNameequal tofieldas "the input yielded no identifier" and refuses to submit.genGroupKeynever reaches the fallback: it callstoFieldNameonly when the label has an ASCII letter or digit, and usesgroupotherwise.ObjectFormCanvasre-exports it, and nothing in the repository imports that re-export.The new function lives in a module that the package's
exports(.only) and itssrc/index.tsbarrel do not name. So the package entry is unchanged, as theClause-②line says.Files:
ReportDefaultInspector.tsxandDatasetDefaultInspector.tsx(the import, thenameFollowsLabelcomparison, the Label handler's derivation, and comments),previews/object-fields-io.ts(the new function, withtoFieldNamedelegating to it), one new test beside each inspector, atoFieldNameOrEmptyblock inobject-fields-io.test.ts, and.changeset/12789-report-dataset-label-leaves-name-empty.md(@object-ui/app-shellpatch).Browser readings (Chromium, real keystrokes)
Backend: objectstack
66d456d01examples/app-crm,objectstack dev --seed-admin --freshon a private port. A writable package was created withPOST /api/v1/packages{"manifest":{"id":"com.qa.vrf",…,"namespace":"qavrf"}}→ 201. Console: this branch's worktree on Vite, proxied to that backend. "Before" was read at8df2608(unchangedmain, before any edit). "After" was read ate4b532b, on the same dev server after the edit. Pages:/apps/crm_app/metadata/{report,dataset,object}/new?package=com.qa.vrf.The sequence on a fresh form: type the Label
收入, then Tab. Then clear the Label. Then typeRevenue BoardandQinto the cleared Label.收入, before → afterfield→ emptyfield→ emptyrevenue_board/revenue_board_q(unchanged)field→ emptyfield→ emptyrevenue_board/revenue_board_q(unchanged)revenue_board/revenue_board_q(unchanged)On a second fresh form, the Label was typed as
Revenue, then replaced with收入, then withRevenue Board. The Name readrevenue→field→revenue_boardbefore, andrevenue→ empty →revenue_boardafter, in both designers. The object designer readrevenue→ empty →revenue_boardboth times.What Save says (Label
收入, the Name left as derived, Save pressed)8df2608): Namefielde4b532b): Name emptyPUT /api/v1/meta/report/field?mode=draft&package=com.qa.vrf→ 422. The name passed; the request was refused only because no dataset was bound ("a report needsdataset+values(measure names).")PUT /api/v1/meta/dataset/field?mode=draft&package=com.qa.vrf→ 200, "Saved dataset 'field' (env-wide, package=com.qa.vrf, state=draft)"^[a-z_][a-z0-9_]*$)So an empty Name asks the author for one: the host's save path refuses an empty identity ("A name is required.") before any request is sent, and the client spec check names the identifier rule.
Tests
New, one beside each inspector. Each renders the inspector with a host double that merges every patch back into the draft, and types real keystrokes with user-event:
ReportDefaultInspector.labelLeavesNameEmpty-12789.test.tsxDatasetDefaultInspector.labelLeavesNameEmpty-12789.test.tsxEach file pins five things:
收入leaves the Name empty, in the box and in the draft.Revenuederivesrevenue, and clearing the Label empties it.收入still follows a Latin Label typed later (Revenue Boardderivesrevenue_board).fieldwhile the Label is收入staysfieldafter a later Latin Label.2024 Revenuederivesf_2024_revenue, and the LabelFieldderivesfield.object-fields-io.test.tsgains atoFieldNameOrEmptyblock. It returns''wheretoFieldNamereturnsfield(empty, blank,收入,报修工单, punctuation only), and the same value astoFieldNameon every input that yields a name.Run at
e4b532b, after the closure build, in repo-root form:pnpm exec vitest runover every file whose name starts withReportDefaultInspector,DatasetDefaultInspectororObjectDefaultInspector, everyobject-fields-iotest,ResourceEditPage.nameSurvivesLabel-12680.test.tsx, and bothCreateItemDialogtests (the caller that relies on the fallback) →Test Files 23 passed (23),Tests 235 passed (235).Red on unchanged main, and the ablations
Each leg ran the two new inspector test files (10 tests); A5 also ran
object-fields-io.test.ts. Every mutation went throughablation-replace.mjsfrom the objectstack checkout: the anchor was hit once, the replacement landed, and the blob changed on disk. R0 replaced whole files instead, checked by blob hash against8df2608. Every restore was proven by a blob hash equal toHEADand an emptygit diff HEAD, and the tree was clean at the end (0 changed paths). The tests import the inspectors andobject-fields-iothrough relative source paths, so nodist/build sits between a mutation and its run.8df2608blobs (toFieldNameOrEmptycount 0)fieldwhere an empty Name is expected)toFieldNameOrEmpty(v) || 'field'nameFollowsLabelcomparison gets the placeholder back:(toFieldNameOrEmpty(…) || 'field')fieldwithrevenuefieldas "no name":toFieldNameOrEmpty(v) === 'field' ? '' : toFieldNameOrEmpty(v)Fieldleft the Name emptyobject-fields-io.ts)toFieldNameOrEmptyreturns'field'instead of''Two more legs stayed green, and each green has a cause:
toFieldNameLoose. Both rulestrim()first, so they differ only on trailing punctuation, which none of these pins type. That difference is pinned by objectui#12746's pin 3 (Revenue -- Boardderivesrevenue_board). Its file is in the suite run above, and PR fix(app-shell): a report's or dataset's Name keeps every underscore typed (objectui#12746) #12788 ablated it.nameValue === ''arm dropped fromnameFollowsLabel. With the comparison now ontoFieldNameOrEmpty, an empty Name next to a non-Latin Label equals the Label's derivation, so pin 3 holds through the comparison alone. The arm still matters for a Name the author clears next to a Latin Label, which these pins do not cover.Local gates (all at
e4b532b)pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' run build→ exit 0 (scope 29 of 47 workspace projects).pnpm --filter @object-ui/app-shell run type-check(tsc --noEmit && tsc -p tsconfig.test.json) → exit 0.--listFileson the test project lists both new test files andobject-fields-io.test.ts.pnpm exec eslinton the six changed source and test files → exit 0, 0 errors. All 7 warnings fall on lines this diff does not touch (react-refresh/only-export-components,@typescript-eslint/no-explicit-any).node scripts/check-changeset-presence.mjs→ exit 0: "6 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)".pnpm check:new-line-citations→ exit 0,VERDICT new-cross-file-line-citations: 0 new citation(s).CI runs everything else.
Acceptance notes
CreateItemDialog(studio-design) still usestoFieldName'sfieldas a sentinel (finalName !== 'field'), so it also refuses an input whose real identifier isfield, such as the LabelField. I read this from source and did not reproduce it in a browser. Nobody is carrying it.toFieldNameOrEmptywould replace the sentinel if anyone takes it up."object":"example_object", the spec create seed's placeholder object. Reading:PUT /api/v1/meta/dataset/probe_ds?mode=draft&package=com.qa.vrfwith body{"name":"probe_ds","label":"Probe Ds","object":"example_object",…}→ 200, while the Base object box read "Select an object…". This change does not touch it.Generated by Claude Code