@tristan2828/ui-foundation 4.25.0 → 4.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -153,7 +153,8 @@ is a filled-in example while the Widgets demo is still here).
153
153
  controls named by position ("Item 2 text"), a per-item `FieldError`
154
154
  (a server 422 at `["body", "checklist", 2, "text"]` arrives as
155
155
  `fieldErrors["checklist.2.text"]` and binds to that row), and a summary
156
- in the table, never the items — and a `computed` field copies
156
+ in the table, never the items (an item with a `reference` field
157
+ copies Pairings, below) — and a `computed` field copies
157
158
  Progress (`checklistState`): `readOnly` in `openapi.yaml` and absent
158
159
  from `<Entity>Create`/`<Entity>Update`, one SQL expression in the
159
160
  router that the filter and sort use (correlated `EXISTS`, or a scalar
@@ -186,6 +187,66 @@ is a filled-in example while the Widgets demo is still here).
186
187
  Select is (Storybook `app/DateField` EditInPlace). The backend column
187
188
  is a SQL `date` (`datetime.date` in the model).
188
189
 
190
+ **A `reference` inside a sub-record item** (a list whose items each
191
+ point at another entity's record) copies Pairings, Widget's
192
+ `pairings`: Checklist's list, with a reference in each item.
193
+ - *Wire:* the item carries the referenced id only, named as a
194
+ top-level reference is (`{ categoryId, note }`, `seriesId` for a
195
+ Series), never a name or a nested record. Every name, on the form
196
+ and on the view, comes from the other entity's lookup by id
197
+ (`GET /<other>?ids=…`), as for a `multi reference`; add `ids` to
198
+ its list endpoint if it lacks one. An optional text field beside it
199
+ is a required `string` that is `""` when empty (`note`), so an item
200
+ never holds a null. `openapi.yaml` describes the item as its own
201
+ schema (`WidgetPairing`), with `maxItems` on the list.
202
+ - *Backend:* a child table like the checklist's (`widget_pairings`:
203
+ `position`, and a foreign key to the referenced table, commented),
204
+ replaced whole on every write. Before saving, the router checks every
205
+ item's id in one query (`_check_item_categories_exist`): one that
206
+ isn't there, or, for a per-user entity, isn't this user's, is a 422 at
207
+ the item's own path, `["body", "pairings", 2, "categoryId"]`, never a
208
+ foreign-key 500. Its tests: order kept with repeats, replace and
209
+ clear, the note trimmed, an unknown id refused at its item on create
210
+ and on update with nothing saved, `null` and too many refused.
211
+ - *The mocks* refuse an unknown id at the same path, and trim the note.
212
+ - *The form's row* is one component shared with editing in place
213
+ (`WidgetPairingItemFields` in `widget-fields.tsx`): the referenced
214
+ record's combobox (`WidgetCategoryCombobox`, the form's Category
215
+ combobox with its own search per row, a lookup by id for the picked
216
+ record's name, and the picked name not counted as a search, so
217
+ opening the list offers every record) and the other fields, each
218
+ named by position ("Pairing 2 category"). On the form it sits in a
219
+ `Controller` over `<list>.<i>.<field>Id`, the rest `register`ed, and
220
+ a server 422 at that path (`fieldErrors["pairings.2.categoryId"]`)
221
+ binds to that row's picker. A new row starts with no pick, which the
222
+ schema refuses on that row ("Pick a category or remove the
223
+ pairing"), so a blank item is never sent.
224
+ - *The view* shows each item as the referenced record's name, from the
225
+ view's one lookup by id (add every item's id to the ids it already
226
+ looks up), then its other fields. The name links to the record's
227
+ view when that entity has one (`<Link to={`/<other>/${id}`}>`);
228
+ WidgetCategory has none, so Pairings shows it as plain text.
229
+ - *Editing in place* is `editInPlace({ kind: 'list', ... })` like
230
+ Checklist's (`pairings` in `widgetEdits`): `renderItem` is the
231
+ form's row, `renderShown` the item as the view shows it, `newItem`
232
+ starts with no pick. In the row, Enter in the open list picks
233
+ without saving; Enter in the row's text box saves the item, as does
234
+ leaving the row (a click on an option is still inside it), and the
235
+ item being added opens again after an Enter, as Checklist's does.
236
+ - *Specs* (copy the `pairings` tests): on the form, saved picks load
237
+ by name, a pick found by searching saves, a row with no pick is
238
+ caught on that row, and a server 422 at the item's reference binds
239
+ to that row's picker; on the view, the names and the sparse record's
240
+ not-set label; in place (`the pairings edited in place`), an item
241
+ added from the keyboard (search, Enter picks, Enter saves) with the
242
+ PATCH sending ids, an item's reference changed by a click, move and
243
+ remove, no pick never sent, and a refused id staying open with the
244
+ reason; and the in-place a11y block for a row open and refused.
245
+ - *Not built:* a "New …" option in the item's picker that creates the
246
+ referenced record inline. Create it on its own screen (or the
247
+ table's `InlineCreate`) first. AI chats (MCP) print an item's
248
+ reference as its id, not its name.
249
+
189
250
  **A `yes/no` marked `toggle`** copies In Stock's cell
190
251
  (`in-stock-toggle.tsx`, cell pattern 15): a `Switch` in its own
191
252
  component that PATCHes the one field through `useSaveWidgetField`, the
@@ -389,7 +450,8 @@ is a filled-in example while the Widgets demo is still here).
389
450
  shared with the form (`WidgetChecklistItemFields`), the item as the
390
451
  view shows it (its quick action, if any) as `renderShown`, the list's
391
452
  schema, and `save: (change) => edit.mutateAsync((current) => ({
392
- <field>: change(current.<field>) }))`. **With no `Edit in place`
453
+ <field>: change(current.<field>) }))` (an item with a reference:
454
+ copy `pairings` in `widgetEdits`, above). **With no `Edit in place`
393
455
  line, nothing is editable in place** and none of this is copied.
394
456
  Never a computed field.
395
457
 
@@ -989,6 +989,13 @@ Checklist is the reference (`checklist` in `widgetEdits`). It's built on
989
989
  about the list (too many items) under the row being added.
990
990
  - `content` shows above the items (the done-count) and `emptyLabel` in
991
991
  place of them when there are none.
992
+ - **An item with a reference** (Widget's Pairings, `pairings` in
993
+ `widgetEdits`) reads as the referenced record's name, from the view's
994
+ lookup by id, and its row is the form's: a combobox per row, with its
995
+ own search. Enter in the open list picks without saving, a click on an
996
+ option is still inside the row, and Enter in the row's text box or
997
+ leaving the row saves the item, ids only on the wire. A new item starts
998
+ with no pick, which the list's schema refuses on that row.
992
999
 
993
1000
  ```tsx
994
1001
  const checklist = editInPlace<ChecklistItem>({
@@ -96,9 +96,28 @@ per list; delete the section if there are none.
96
96
  | done | Done | yes/no | yes | default no |
97
97
 
98
98
  - Item fields use the same types as the main table (`text`, `url`,
99
- `yes/no`… — not another sub-record or a reference). Items have no screen
100
- of their own: they're added, edited, reordered and removed on the
101
- record's form, and the order is kept.
99
+ `yes/no`…), including a `reference` (below), but not another
100
+ sub-record, a `multi reference` or a `multi choice`. Items have no
101
+ screen of their own: they're added, edited, reordered and removed on
102
+ the record's form, and the order is kept.
103
+ - **A `reference` item field** points each item at a record of another
104
+ entity: name it, as for a top-level reference (`seriesId` → Series).
105
+ Each row picks it with the same searchable combobox; the item carries
106
+ the referenced id only, and every name comes from the other entity's
107
+ lookup by id (its list endpoint must accept `ids`). Say whether it's
108
+ required, and whether the same record may appear in more than one
109
+ item. On the view each item reads as the record's name, linked to its
110
+ view when it has one. An optional text item field beside it is `""`
111
+ when empty. Widget's `pairings` is the worked example
112
+ (`docs/entities/widget.md`):
113
+
114
+ | Field | Label | Type | Required | Options / rules |
115
+ |---|---|---|---|---|
116
+ | categoryId | Category | reference → WidgetCategory | yes | picked from a searchable list; repeats allowed |
117
+ | note | Note | text | no | up to 100 chars, trimmed; empty means none |
118
+
119
+ Not supported yet: creating the referenced record from the item's
120
+ picker (a "New …" option); create it on its own screen first.
102
121
 
103
122
  ## List screen
104
123
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tristan2828/ui-foundation",
3
- "version": "4.25.0",
3
+ "version": "4.27.0",
4
4
  "description": "Shared UI foundation for contract-first, database-backed React apps: shadcn primitives, DataTable/EntityForm composites, auth, the gateway error seam, design tokens, lint rules and the AI playbook.",
5
5
  "license": "MIT",
6
6
  "repository": {