@tristan2828/ui-foundation 4.26.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
|
|
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>) }))
|
|
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`…
|
|
100
|
-
|
|
101
|
-
|
|
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.
|
|
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": {
|