Repository navigation
Redo the record-list "Add View" create flow: empty-name 405, invisible draft views, canonical naming (supersedes PR #2754) #2767
Description
Background
PR #2754 attempted to fix two real bugs in the record-list page's "Add View" flow, but the implementation has several structural problems (detailed below). The decision is to redevelop from scratch — this issue is the full re-implementation spec. Close #2754 once this lands.
The two original problems to solve
- Empty-name 405 — when the create-view dialog produced an empty name, the client sent
PUT /api/v1/meta/view/(empty:namesegment), which the server rejects with 405. - Newly created views "vanish" — per ADR-0034, runtime-created views are written to the
/metaoverlay as drafts, whilelistViews()only reads published items. After clicking "Create", the view never appears in the ViewTabBar and there is no way to verify it before publishing.
Problems in PR #2754's implementation (must be avoided in the redo)
P1 (most severe): view identity split — row key vs body.name disagree
The PR builds viewBody.name = "${objectName}.${draftName}" (qualified name) but calls createRuntimeMetadata('view', draftName, viewBody) with the bare name as the URL segment. The server's saveMetaItem (framework packages/metadata-protocol/src/protocol.ts ~L4107) keys the sys_metadata row by the URL segment; body.name is just data. The canonical Studio flow (ResourceEditPage.tsx doSave) uses the body's identity field as the URL segment, i.e. row key = qualified name <object>.<key> (see the view createBuildBody in metadata-admin/anchors.ts).
Consequences of the split: listViews() flattens tab ids to the qualified name, but every by-name item-level read/write — readRuntimeDraft / publishRuntimeMetadata / discardRuntimeDraft (the RuntimeDraftBar unpublished-changes indicator and per-item Publish), the Studio edit page, history/layered — looks up the qualified name against a bare-name row → 404. The ADR-0034 draft→publish loop is broken; a later Studio save under the qualified name creates a second metadata row for the same view.
P2: wrong draft-merge semantics — "append" instead of "overlay"
The server's GET /meta/view?preview=draft already returns the active+draft overlaid list (draft wins by name, tagged _draft: true — see the previewDrafts branch of getMetaItems in framework protocol.ts). The PR instead makes two requests (normal list + preview list), filters _draft === true, and appends. When a draft edits an existing published view (same name), both versions land in the list; the dedup set metaIds in ObjectView.tsx is computed once before the merge loop and never updated as items are pushed → duplicate tabs with the same id.
P3: bypasses the existing plumbing
The PR hand-writes fetchImpl(baseUrl + '/api/v1/meta/view?preview=draft') inside the adapter: it hardcodes the route (the SDK resolves it via getRoute('metadata')) and drops environment scoping (the /api/v1/environments/:id/meta path form). The same package already ships the purpose-built MetadataClient.withPreviewDrafts(true).list('view') (packages/data-objectstack/src/metadata-client.ts).
P4: the main-path symptom remains unsolved
The "Add View" button lives on the normal runtime page. In non-preview mode: create → draft written → listViews (published-only) doesn't include it → navigate('view/<createdId>') fails to resolve and falls back to the default view. The user-visible symptom ("the view I just created vanished") is unchanged. Only users who already know to manually append ?preview=draft benefit; the page offers no entry into preview mode and no post-create guidance.
P5: CJK labels always fall into the random-name fallback
slugify (metadata-admin/createDerive.ts) is NFKD + strip [^a-z0-9], so a Chinese label reduces to an empty string → every Chinese-named view gets a random name like task_grid_mrsyt56j. For a Chinese-first product, the "fallback" becomes the common path.
P6: duplicated code
The ~25-line "derive name + wrap ViewItem envelope" block is copy-pasted into both ObjectView.tsx and ObjectDataPage.tsx. The runtime-metadata-persistence.ts seam exists precisely to absorb this (compare recordPageEnvelope for the page type).
Re-implementation plan
1. Empty-name guards (the part of #2754 the review already approved — keep the idea)
- Add non-empty-name early-exit validation to both
MetadataClient.save()(packages/data-objectstack/src/metadata-client.ts) andcreateRuntimeMetadata()(packages/app-shell/src/views/runtime-metadata-persistence.ts), throwing a clear error instead of emitting a malformed URL.
2. Unify identity: use the qualified name as the URL segment
- Collapse envelope construction into a single helper in
runtime-metadata-persistence.ts(e.g.viewEnvelope(objectName, spec, name?)plus name derivation), shared byObjectView.tsxandObjectDataPage.tsx. - Emit a canonical ViewItem:
{ name: '<object>.<key>', object, viewKind: 'list', label, config }withconfig.data = { provider: 'object', object }(aligned withcreateBuildBodyinanchors.ts; do not leave dialog noise fields likelabel/nameinsideconfig). - Save with the qualified name as the URL segment:
createRuntimeMetadata('view', '<object>.<key>', envelope), guaranteeing row key =body.name= tab id.
3. Draft visibility: full replacement in preview mode
listViews(objectName, { previewDrafts }): whenpreviewDraftsis true, make one request viaMetadataClient.withPreviewDrafts(true).list('view')(or add a preview option to the framework client SDK'smeta.getItems) and use the result to replace the base list entirely (the server already performs the draft-wins overlay). No dual-request append.- On the
ObjectViewside, take the gate fromusePreviewDrafts()(preview/PreviewModeContext.tsx) and include it in the effect deps. Normal runtime mode sees published views only, preserving the ADR-0033 publish gate and ADR-0037 preview gating. - Optional nice-to-have: pass the
_drafttag through to the ViewTabBar for a draft badge instead of dropping it during flattening.
4. Close the main-path loop (the core user value)
After a successful create in non-preview mode, pick one (a recommended):
- a. Auto-navigate to the new view's route with
?preview=draft— the user lands on it with the DraftPreviewBar visible, can verify immediately, and publish in one click; - b. Show a toast stating the view was staged as a draft, with a link to preview/publish.
5. CJK-aware naming
- Add an editable machine-name (
name) field toCreateViewDialog: prefill viaslugifyfor Latin labels (user-editable); when slugify yields an empty string (e.g. a pure-Chinese label), require the user to fill it in; keep the random fallback (<object>_<type>_<base36 timestamp>) only as a last resort.
Acceptance criteria
- An empty name no longer produces a 405; a clear, contextual error is thrown instead.
- For a newly created view, the
sys_metadatarow key,body.name, and the ViewTabBar tab id are identical (all<object>.<key>);RuntimeDraftBarcan read the draft and per-item Publish succeeds. - In preview mode (
?preview=draft): a newly created draft view appears in the ViewTabBar immediately; a draft edit of an existing published view does not produce a duplicate tab (the draft version wins). - In normal mode drafts stay invisible (no leakage to ordinary viewers), but the create flow provides clear follow-through (auto-enter preview, or a toast).
- Chinese labels get a user-controllable machine name; no mass random names.
- No raw
fetchof metadata routes at the adapter layer; environment-scoped setups do not regress. - No duplicated envelope-construction code between
ObjectView.tsxandObjectDataPage.tsx. - Existing tests stay green; add unit tests for the
listViewspreview branch, the envelope helper, and the empty-name guards.
References
- Superseded PR: fix: empty view name 405 + draft views not appearing in view switcher #2754 (including both review rounds)
- ADR-0033 (draft/publish gate), ADR-0034 (runtime metadata persistence seam), ADR-0037 (Live Canvas /
?preview=draft, frameworkdocs/adr/) - Key files:
packages/app-shell/src/views/ObjectView.tsx,ObjectDataPage.tsx,runtime-metadata-persistence.ts,RuntimeDraftBar.tsx,CreateViewDialog.tsx,preview/PreviewModeContext.tsx,metadata-admin/anchors.ts,metadata-admin/createDerive.ts,packages/data-objectstack/src/index.ts(listViews),metadata-client.ts,packages/types/src/data.ts(DataSource.listViewssignature)
Activity
[-]重做记录列表页 "Add View" 创建流程:空名 405、草稿视图不可见、身份命名规范(取代 PR #2754)[/-][+]Redo the record-list "Add View" create flow: empty-name 405, invisible draft views, canonical naming (supersedes PR #2754)[/+]fix: redo record-list "Add View" flow — empty-name 405, invisible dra…