Skip to content

Redo the record-list "Add View" create flow: empty-name 405, invisible draft views, canonical naming (supersedes PR #2754) #2767

Description

@os-zhuang

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

  1. Empty-name 405 — when the create-view dialog produced an empty name, the client sent PUT /api/v1/meta/view/ (empty :name segment), which the server rejects with 405.
  2. Newly created views "vanish" — per ADR-0034, runtime-created views are written to the /meta overlay as drafts, while listViews() 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) and createRuntimeMetadata() (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 by ObjectView.tsx and ObjectDataPage.tsx.
  • Emit a canonical ViewItem: { name: '<object>.<key>', object, viewKind: 'list', label, config } with config.data = { provider: 'object', object } (aligned with createBuildBody in anchors.ts; do not leave dialog noise fields like label/name inside config).
  • 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 }): when previewDrafts is true, make one request via MetadataClient.withPreviewDrafts(true).list('view') (or add a preview option to the framework client SDK's meta.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 ObjectView side, take the gate from usePreviewDrafts() (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 _draft tag 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 to CreateViewDialog: prefill via slugify for 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_metadata row key, body.name, and the ViewTabBar tab id are identical (all <object>.<key>); RuntimeDraftBar can 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 fetch of metadata routes at the adapter layer; environment-scoped setups do not regress.
  • No duplicated envelope-construction code between ObjectView.tsx and ObjectDataPage.tsx.
  • Existing tests stay green; add unit tests for the listViews preview 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, framework docs/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.listViews signature)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Fields

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions