Repository navigation
Commit bee5d6f
feat(app-shell): an object leaf's Studio panel edits its list view (objectui#11823, step 1) (#11868)
Refs #11823 (item 1)
Clause-②: no
## What this does
An `object` leaf's Properties panel in Studio's Interfaces pillar now
edits the list view its canvas shows: **Columns** (add, remove, drag, or
select a row and move it up or down), **Filter** and **Sort**. Edits
autosave to the package draft and show on the canvas at once: ADR-0084's
"source + preview", with preview = runtime. Steps 2 (dashboard and
report create flow) and 3 (page create flow) of objectui#11823 are not
in this PR, and the card stays open for them.
- **Which view.** The one the canvas opens, read off the nav entry as
the runtime reads it (`navEntryListTarget`). An entry that lands on a
named view edits that view (matched with `resolveViewId`; a name that
matches nothing is `OBJECT.NAME`, created by the first edit). Any other
entry edits the object's default list view: the merged view flagged
`isDefault`, else `OBJECT.default`, the name `expandViewContainer` gives
a container's default `list`.
- **What is written, and through what.** A `view` item in its record
shape, `{ name, object, viewKind: 'list', isDefault?, config: { type,
data, columns, filter?, sort? } }`, sent by `client.save('view', name,
body, { mode: 'draft', packageId })`: the pillar's own `MetadataClient`,
behind its own `useDraftSaveGuard` (objectui#11773), debounced by the
pillar's `useDraftAutoSave`. Both are called as they are. No new
endpoint and no new client.
- **A view that does not exist yet.** The panel says so, and shows the
columns the running app shows such an object
(`defaultListColumnsFromObject`, imported from `views/ObjectView.tsx`).
The canvas shows those columns too. The first edit creates the view from
them.
- **Read-only package.** Only the read-only note shows, every control is
disabled, the autosave is blocked on the same `readOnly`, and nothing is
written.
- **Not a list view.** An item at the view's name that is not a list
view (a form view, or a body with no `columns`) is shown as such, and is
never edited or saved over.
- **A refused save** reads as the pillar reads one: `issueRefusal` from
`metadataError.ts` (imported, not edited) gives the author sentence,
with the raw refusal under Details.
- **Control.** A studio-canvas leaf of any other type keeps
`studioCanvasNoBlocks`.
## Measurements (H1 to H4)
Taken against a published `@objectstack/*` 17.7.0 server (`objectstack
dev --fresh`, the release this repo resolves), with a writable Studio
package (`POST /api/v1/packages`) and a code package, and this branch's
console under Vite, driven by Playwright and Chromium.
- **H1: confirmed, with one more finding.** On `59ce048` the panel of an
object leaf showed only `studioCanvasNoBlocks` (browser reading). For a
plain entry the canvas rendered `{ type: 'object-view', objectName }`
with no `listViews`. That is a grid of the object's fields (Title,
Amount, Stage, Due, Owner), not the package's default list view. On the
same data, the running app (`/apps/pst_app/pst_order`) rendered the
published default view: Title and Amount, filtered and sorted. For an
object with no view, the canvas also showed Owner, which the runtime's
fallback list leaves out. For a `viewName` entry, the canvas rendered
the cache's named view. The panel now edits the entry's named view, or
else the default view. The canvas shows the panel's view in both cases.
- **H2: measured; the record shape is written.** The runtime serves an
object's list views as `view` records (`GET /meta/view`). A package's
`defineView` container arrives expanded: `OBJ.default` with `isDefault:
true`, and `OBJ.KEY` for each named view. `GET /meta/object/NAME`
carries no `listViews`; the console merges `view` items into
`objectDef.listViews` (`mergeViewsIntoObjects`). The draft door accepted
both a record and a container body (`PUT
/meta/view/NAME?mode=draft&package=…`, 200). But `?preview=draft` listed
the record and not the container, because containers are never listed.
`publish-drafts` published the record, and `GET /meta/view` then served
it. The door refused a sort entry spelled `direction` (422). It accepted
a draft for a code package's view as a customization overlay, so the
read-only gate is Studio's own `readOnly`, as for every other inspector.
The object's embedded `listViews` (`ObjectSchema.listViews`, meant for
system objects) is not written: it belongs to the object definition and
the Data pillar.
- **H3: confirmed, and resolved inside the canvas body.** The canvas
read only the metadata cache (`useMetadata`, published rows), and only
for a `viewName` entry, so a draft never showed there.
`StudioObjectRecordsCanvas`'s body now also reads a Studio-internal
context, `StudioCanvasListViewContext`. It sits beside the published
props, like `StudioCanvasNavEntryContext`, and holds the panel's buffer.
The function's signature and `StudioCanvasPreviewProps` are unchanged,
and nothing is added to `index.ts`.
- **H4: measured.** The runtime grid's column picker lives inside
`plugin-list`'s `ListView` (`ViewSettingsPopover`, not exported), so it
cannot be imported. Three editors are reused by import, unedited:
- **Columns:** the Studio's `FieldsListEditor`.
- **Filter:** the runtime `FilterBuilder`, through the designer's
exported `filter-builder` widget, which writes the spec's
`ViewFilterRule[]`.
- **Sort:** the runtime `SortBuilder` from `@object-ui/components`,
inside a popover. Placed inline in the 18rem rail, its fixed label and
direction widths leave the field select about 36px. It has no disabled
prop, so the read-only state disables its trigger instead. Its row `id`
is stripped on write, because the spec refuses `id` on a sort entry.
## The saved shape (browser reading, writable package)
After removing one seeded column of an object that had no view:
```text
PUT /api/v1/meta/view/pst_item.default?mode=draft&package=com.probe.studio
{"name":"pst_item.default","object":"pst_item","viewKind":"list","isDefault":true,"config":{"type":"grid","data":{"provider":"object","object":"pst_item"},"columns":["name","qty"]}}
```
Every saved body in the pins parses with the spec's `ViewItemSchema`
(installed `@objectstack/spec` 17.7.0).
## Real-browser reading (dev-only harness, not committed)
Console from this branch, backend `@objectstack/*` 17.7.0, Chromium
(`/opt/pw-browsers/chromium`):
- Orders, which has a published default view: the panel showed `All
Orders` with columns `title`, `amount`, filter Stage, and sort "Amount
descending". The canvas showed Title and Amount, rows Charlie 20 and
Alpha 10, which is the runtime's own rendering.
- Adding the Stage column put Stage on the canvas at once, and an
autosave `PUT
…/view/pst_order.default?mode=draft&package=com.probe.studio` followed.
The header's Changes count went to 1.
- Switching the sort to ascending reordered the canvas to Alpha 10,
Charlie 20, and the PUT carried `"order":"asc"`.
- Changing the filter value to `won` changed the canvas to Bravo 30 and
Delta 5, and the PUT carried
`{"field":"stage","operator":"equals","value":"won"}`.
- Items, which has no view: the panel said no list view exists yet and
showed `name`, `qty`, `color`. The canvas showed Name, Quantity and
Color, as the running app does (before this change, the canvas also
showed Owner). Removing `color` created the view (the PUT above), and
the canvas dropped the column. After the package's drafts were
published, the running app listed Name and Quantity.
- A code package (`writable: false`): the panel was read-only, with no
drag handles, no remove buttons, no Add field button, and disabled
Filter and Sort triggers. No PUT was sent.
- Opening the Filter and Sort popovers wrote nothing.
## File surface
Every file is on the claim's surface:
- `studio-design/ObjectListViewInspector.tsx` (new): the panel, the
buffer hook (load, the version guard, save) and the pure helpers.
- `studio-design/StudioDesignSurface.tsx`:
- the inspector body's studio-canvas branch;
- in `InterfacesPillar`, the list view's draft write path, which the
claim asked to be named before editing: one `useObjectListViewDraft`
call and one `useDraftAutoSave` call, placed right after the leaf
autosave;
- the provider around the canvas;
- the conflict dialog's render;
- an import.
None of seat 3's objectui#11786 regions is edited: the
`useDraftAutoSave` definition, the Data pillar's `doSave`, the
Automations save and `doPublish`.
- `studio-design/studio-canvas-preview.tsx`: the new context,
`listViewsOf` exported at module level (not from `index.ts`), and the
body of `StudioObjectRecordsCanvas`.
- `metadata-admin/i18n.ts`: new `engine.studio.inspector.listView.*`
rows, en and zh.
- Tests beside these, and `.changeset/11823-list-view-inspector.md`
(`@object-ui/app-shell` patch).
Two existing pins changed meaning, because the object leaf's rail is no
longer the no-blocks statement:
- `StudioDesignSurface.studioCanvasLeaf.test.tsx` now takes the
no-blocks pins on a stub `report` studio canvas, and adds an object-leaf
pin for the new panel.
- `StudioDesignSurface.selectionLeafScope.test.tsx` now waits for the
panel instead of the statement. Its assertions are unchanged.
## Tests and gates (local `git rev-parse --short HEAD` = `f5610fa`,
which includes `main` at `5aa7f55`)
| Command | Exit | Verdict |
|---|---|---|
| `pnpm --workspace-concurrency=2 --filter "@object-ui/app-shell^..."
build` (via os-verify-lock) | 0 | `VERDICT command-exit 0`, 29 of 47
projects |
| `pnpm --filter @object-ui/app-shell type-check` (`tsc --noEmit && tsc
-p tsconfig.test.json`) | 0 | `tsc -p tsconfig.test.json --listFiles`
lists all 5 touched test files |
| `pnpm exec vitest run` on `StudioDesignSurface*`,
`studio-canvas-preview*` and `ObjectListViewInspector*` | 0 | `Test
Files 54 passed (54)`, `Tests 341 passed (341)` |
| `pnpm exec vitest run` on the other 44 `studio-design` test files,
plus the 5 tests that read the designer i18n tables | 0 | `Test Files 49
passed (49)`, `Tests 381 passed (381)` |
| `pnpm check:control-bytes` | 0 | `check-control-bytes: OK` |
| `pnpm check:new-line-citations` | 0 | `VERDICT
new-cross-file-line-citations: 0 new citation(s)` |
| `pnpm check:changeset-claims` | 0 | `No pending changeset names a file
this change touches.` |
| `pnpm check:pending-changeset-literals` | 0 | `No test source names a
pending changeset.` |
| `pnpm check:i18n-designer-parity` | 0 | `Every en row has a zh row,
and every shared row carries the same placeholders.` |
| `pnpm check:spec-symbols` | 0 | OK |
| `pnpm check:i18n-keys` / `check:i18n-drift` | 0 / 0 | keys resolve /
`No designer-table en value changed in this range.` |
| `pnpm check:vi-mock-specifiers` / `-inherit` / `-override-shape` | 0 /
0 / 0 | OK |
| `pnpm check:test-path-roots` / `check:metadata-write-doors` /
`check:unreferenced-sources` | 0 / 0 / 0 | OK |
| `node scripts/check-changeset-presence.mjs` /
`check-changeset-no-major.mjs` | 0 / 0 | `9 source file(s) of 1 released
package(s) changed, and this change declares 1 changeset(s)` / no
`major` |
**Lint (narrowed, measured).** `eslint --format json` ran over the 9
touched TS files, as the package script runs it (`eslint .` from
`packages/app-shell`): 9 files, 0 errors, 26 warnings, all of them
`react-refresh/only-export-components` or `react-hooks/*` warnings, of
the kinds the sibling studio-design files already carry. The root
`eslint.config.js` sets no type-aware parser options (`project` /
`projectService`), so this diff cannot move a verdict on a file it does
not touch. The repo-wide `pnpm lint` and the full test farm are CI's.
**Reverse check.** With the fix committed, `StudioDesignSurface.tsx` was
set back to `main`'s blob (`git checkout 5aa7f55 -- …`, under a trap
restoring from `HEAD`). The new pin file then went red in the predicted
direction: `Tests 6 failed | 1 passed (7)`. Every panel pin failed with
`Unable to find an element by:
[data-testid="studio-list-view-inspector"]`, and the rail rendered the
no-blocks statement. The CONTROL pin stayed green. After restoring, the
file's blob equals `HEAD`'s (`9cb25ec…`) and `git diff HEAD` is empty.
## Acceptance notes
- A `viewName` entry naming a view that does not exist yet: the panel
shows the seed columns and the not-created note, while the canvas keeps
its existing fallback (the plain grid) until the first edit. The
runtime's landing for an unresolved `/view/NAME` was not measured.
- `FieldsListEditor` is reused as it is. A row shows a column's own
label, else its field name. The add-field popover still offers a field
that is already a column (badged "Added") and system fields. Adding one
twice writes a duplicate column, which the draft door accepts. Carrier:
none.
- A draft read carries `_packageId`, a spec-declared protection field,
and saves send it back, as the pillar's leaf drafts do.
- A list-view edit left within the 1.5 s debounce is dropped, as the
leaf draft's is. Only nav editing mirrors dirty state up to the header's
leave guard.
- An object of another package, opened from this package's nav, is
edited as a draft under this package. The door accepts that as a
customization overlay; the round trip was not measured.
- No message locator maps a refused view issue to its control yet, so a
refusal reads as "the server refused this draft", with the issues under
Details.
Session: `https://claude.ai/code/session_01DrKzdPdyLLBW3qpZ4vtk7z`
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DrKzdPdyLLBW3qpZ4vtk7z)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent d33270a commit bee5d6f
10 files changed
Lines changed: 1260 additions & 30 deletions
File tree
- .changeset
- packages/app-shell/src/views
- metadata-admin
- studio-design
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3118 | 3118 | | |
3119 | 3119 | | |
3120 | 3120 | | |
| 3121 | + | |
| 3122 | + | |
| 3123 | + | |
| 3124 | + | |
| 3125 | + | |
| 3126 | + | |
| 3127 | + | |
| 3128 | + | |
| 3129 | + | |
| 3130 | + | |
| 3131 | + | |
| 3132 | + | |
| 3133 | + | |
3121 | 3134 | | |
3122 | 3135 | | |
3123 | 3136 | | |
| |||
6099 | 6112 | | |
6100 | 6113 | | |
6101 | 6114 | | |
| 6115 | + | |
| 6116 | + | |
| 6117 | + | |
| 6118 | + | |
| 6119 | + | |
| 6120 | + | |
| 6121 | + | |
| 6122 | + | |
| 6123 | + | |
| 6124 | + | |
6102 | 6125 | | |
6103 | 6126 | | |
6104 | 6127 | | |
| |||
Lines changed: 87 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
0 commit comments