feat(admin-layer): relation pickers, enum-select guard, detail-link hook, 0.2.0 - #158
Conversation
`ontogenAdmin.registry` names the generated admin registry the `#admin-registry` alias resolves to. The default is unchanged — the consuming app's `app/admin/generated/admin-registry` — so a host whose registry is emitted into a Nuxt layer rather than the root app can point the alias at it instead of copying the file.
…ook, 0.2.0 A relation field is a picker over the target entity's rows (type to filter, arrow keys, Enter; the raw id stays behind "Edit id"). The enum select renders only when the registry carries values, else text. A host can provide ADMIN_DETAIL_LINK to link a detail page to its own page for the row. admin-types widens idType to string | number. Both packages are 0.2.0; `just pack-admin` packs them with pnpm into dist/.
…e by page
A paginated listMethod answers { items, total }; calling it bare and
filtering the envelope threw, and the table showed nothing.
…puts it A list that takes no query struct is (limit?, offset?); passing (undefined, limit, offset) asked every page for the default limit at the wrong offset. The registry's listQuery says which shape a list has; pageArgs builds the call from it.
Two statements in an inline handler need a separator; the SFC compiler refused the template.
The edit form seeded a null relation as '' and sent it back, so saving a row with no athlete tripped the foreign key. Blank now means none: an emptied optional field goes out as null, which clears the column (undefined would leave it alone). The layer is 0.2.1.
| * (`listQuery`), and the page goes after it. | ||
| */ | ||
| export function pageArgs(config: AdminEntityConfig, limit: number, offset: number): unknown[] { | ||
| return config.listQuery ? [undefined, limit, offset] : [limit, offset] |
There was a problem hiding this comment.
Bug: listQuery is never emitted, so query-taking paginated lists get the wrong argument shape.
pageArgs falls back to (limit, offset) unless config.listQuery is true, but nothing writes listQuery into a registry: src/clients/generators/admin.rs on this branch doesn't emit it at all, and main (6349a9d) emits the same flag under the name listHasQuery.
Scenario: a paginated entity whose Rust list takes a query struct gets the transport signature list(query?, limit?, offset?) (transport.rs puts query? first whenever a param type contains Query). Before this PR useAdminEntity always called fn(undefined, limit, offset); now it calls fn(limit, offset), so limit is sent as the query struct and offset as the limit — the admin list, relation picker and inverse relation tables all page wrongly. After merging with main the registry will say listHasQuery: true while this reads listQuery, so the regression survives the merge.
Suggested fix: rename to listHasQuery (matching main's generator and admin-types) and drop the branch-local listQuery from admin-types/index.ts.
There was a problem hiding this comment.
Fixed in ed52ebd: pageArgs now reads config.listHasQuery, and admin-types/index.ts carries main's listHasQuery declaration (same doc comment and position) in place of the branch-local listQuery so the merge with main is clean.
| } | ||
|
|
||
| function onKeydown(event: KeyboardEvent) { | ||
| if (!open.value && (event.key === 'ArrowDown' || event.key === 'Enter')) { |
There was a problem hiding this comment.
Enter on a closed picker submits the enclosing form.
When the listbox is closed, Enter only sets open = true and returns without event.preventDefault(), so the same keypress also submits the surrounding edit/create form.
Scenario: focus the picker (opens), press Escape to dismiss the list, then press Enter to reopen it — the list flashes open and the form submits.
| if (!open.value && (event.key === 'ArrowDown' || event.key === 'Enter')) { | |
| if (!open.value && (event.key === 'ArrowDown' || event.key === 'Enter')) { | |
| event.preventDefault() |
There was a problem hiding this comment.
Fixed in ed52ebd: the closed-picker ArrowDown/Enter branch now calls event.preventDefault() before opening the list, so Enter no longer submits the enclosing form.
… Enter on a closed picker does not submit the form The generator emits listHasQuery, not listQuery, so pageArgs never saw the query-taking shape and sent limit as the query struct. The picker's closed-state Enter now prevents default so it only reopens the list.
Eight specs authored in the consuming dev monorepo against ontogen's source — six of them already in review here as #156, #158, #159, #160, #161 and #166 — move into docs/planning/tasks/ so the reasoning behind each change lives with the generator rather than with one consumer. Rewritten upstream-native: repo-relative paths, the consumer's situation as motivation rather than scope, and the consumer-side acceptance criteria (pin bumps, drift gates, fallback plans) left behind in the monorepo where they belong. Two carry no implementation yet: the schema field-type vocabulary (bytes, timestamp, date, JSON, defaults, declared indexes) and explicit route shapes. The consumer's ninth item, per-variant HTTP error status, is already covered by the E0003 epic and is not duplicated here.
Main independently extracted into helpers the same logic this branch wrote inline, so each conflict is a choice between two spellings of one idea: - AdminRelationTables: main's `fetchAllItems` replaces the branch's inline paging loop. It already handles `listHasQuery`, and adds the guards the loop lacked — a ceiling for a `total` that keeps moving under concurrent inserts, and a hard 1000-request cap. - edit.vue: main's `toUpdateInput` replaces the branch's inline null coercion. Its `isBlank` also treats `null` as empty, which the branch's `'' || undefined` check missed. - useAdminEntity: keeps the branch's `pageArgs` helper for the call shape (identical to main's inline ternary, and still used by the relation picker) and takes main's stale-response guard and page clamp-and-retry. - package.json: the branch's deliberate 0.2.x feature bump over main's 0.1.1. The merge then combined the branch's auto-imported helpers with main's test files, which stub globals per file and knew nothing about them, so `pageArgs` and `useAdminDetailLink` were undefined under vitest. Both are pure — a plain function, and an inject with a null-returning default — so they join `ref`/`computed` in tests/setup.ts, which exists to stand in for exactly these auto-imports.
Stacks on rust-ontogen#157 (the registry path option).
AdminRelationPicker.vue: arelationfield lists the target entity's rows through itslistMethod(first page for a paginated target), shows them asid — name, narrows as you type, and takes ArrowUp/ArrowDown/Enter/Escape. The raw id stays editable behind Edit id.relation-arraykeeps the chip input.<select>renders only whenfield.enumValuesis non-empty; a registry without values falls back to a text input.useAdminDetailLink.tsexportsADMIN_DETAIL_LINK, an injection key for(entity, id) => { to, label } | null. A host that provides it gets a link in the detail page header next to Edit.idTypeis'string' | 'number'.^0.2.0(still optional, since bun auto-installs peers and the package is not on npm).just pack-adminpacks both with pnpm intodist/; pnpm rewrites the layer'sworkspace:*devDependency to the version on the way out, so the tarball carries no workspace protocol.Verified by vendoring the tarballs into a consumer (
bun install, its registry unit test, and its Nuxt typecheck pass).