@ghentcdh/crouton-core 0.0.1-alpha.85 → 0.0.1-alpha.87

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.
@@ -326,6 +326,9 @@ const router = createRouter({
326
326
 
327
327
  const app = createApp(App);
328
328
  app.use(CroutonPlugin(api, { title: 'My App', router }));
329
+
330
+ // To restrict supported languages to English only:
331
+ // app.use(CroutonPlugin(api, { title: 'My App', router, i18n: { languages: ['en'] } }));
329
332
  app.use(router);
330
333
  app.mount('#app');
331
334
  ```
@@ -25,7 +25,7 @@ Both forms support the same options:
25
25
  | `createable` / `updateable` | Whether the field is written on create/update |
26
26
  | `required` | Whether the **form** demands a value. Overrides the Zod model in either direction on a prisma resource (`true` adds, `false` removes, omitted defers to the model); the only way to mark a field required on a `kind: "custom"` resource. Ignored on the id column and on fields that are neither createable nor updateable |
27
27
  | `displayKey` | Nested field to display (e.g. `author.name`) |
28
- | `showInLookup` | Sets the display label when **this** resource appears as an option in another resource's autocomplete. Does **not** affect `?q=` search — use `layout.table.searchFields` for that. |
28
+ | `showInLookup` *(deprecated)* | **Use `lookup.labels` at the resource root instead** (run `crouton migrate`). Sets the display label when this resource appears as an autocomplete option in another resource. Does **not** affect `?q=` search. |
29
29
  | `unique` | Validate the value is unique: `true`, or `{ scope, caseInsensitive, message }`. Checks as-you-type and maps a DB unique violation to this field. Needs a matching `@unique` constraint. See [Unique fields](unique.md) |
30
30
  | `column` | Source column name when it differs from the key/`id` (defaults to `id`) |
31
31
  | `enum` | Name of a shared enum in `crouton.enums.json`; its `{ value, label }[]` is injected into `fieldInput.options.values` |
@@ -35,7 +35,8 @@ Both forms support the same options:
35
35
  ::: warning Moved to `layout.table`
36
36
  The following column-level options are **deprecated** and will be removed. Use `layout.table` instead:
37
37
 
38
- - `sortable` / `defaultSort` → `layout.table.items[{ id, sortable, defaultSort }]`
38
+ - `sortable` → `layout.table.items[{ id, sortable }]`
39
+ - `defaultSort` → `layout.table.defaultSort` (`{ field: "colId", sortDir: "asc" | "desc" }`)
39
40
  - `searchable` → `operations.findAll.search: ["colId"]`
40
41
  - `filterable` → `layout.table.filterFields: ["colId"]`
41
42
  - `showWhen` / `hideWhen` / `disabledWhen` → `layout.table.items[{ id, showWhen, ... }]`
@@ -217,57 +218,65 @@ Add `customComponent` to the table layout item's `options` to override the cell
217
218
  The custom cell component receives `data` (full row object), `value` (cell value), `column`, and `options` as props.
218
219
  See [Custom renderers](../4.frontend/custom-renderers.md) for how to register the component.
219
220
 
220
- ## Calculated columns
221
+ ## Computed columns
221
222
 
222
- Read-only columns computed in SQL at query time. Use `main` as the alias for the resource's own table:
223
+ Read-only columns derived from a raw SQL subquery at query time. Add a `computed` object to the column definition:
223
224
 
224
225
  ```json
225
226
  {
226
- "calculatedColumns": [
227
- {
228
- "id": "chapter_count",
229
- "alias": "chapter_count",
227
+ "columns": {
228
+ "chapter_count": {
230
229
  "label": "Chapters",
231
- "type": "number",
232
- "position": 5,
233
- "sqlExpression": "(SELECT count(*) FROM chapter c WHERE c.book_id = main.id)"
230
+ "computed": {
231
+ "sql": "(SELECT count(*) FROM chapter c WHERE c.book_id = main.id)",
232
+ "type": "number"
233
+ }
234
234
  }
235
- ]
235
+ }
236
236
  }
237
237
  ```
238
238
 
239
- | Field | Type | Description |
240
- |-----------------|----------|-----------------------------------------------------------|
241
- | `id` | `string` | Column id |
242
- | `alias` | `string` | SQL alias — must match `id` in most cases |
243
- | `label` | `string` | Display label |
244
- | `type` | `string` | Data type shorthand (`"number"`, `"string"`, etc.) |
245
- | `position` | `number` | Order among columns |
246
- | `hiddenInTable` | `boolean`| Hide from table (still available via API) |
247
- | `sqlExpression` | `string` | The SQL subquery expression. Use `main` for the parent table alias. |
239
+ | Field | Type | Description |
240
+ |--------------|----------|-----------------------------------------------------------------------------|
241
+ | `sql` | `string` | Raw SQL subquery. Use `main` as the alias for the parent table row. |
242
+ | `type` | `string` | Cast type for the result: `"number"` (default), `"string"`, or `"boolean"`. |
243
+
244
+ All standard column fields (`label`, `hiddenInTable`, `hiddenInView`, `position`, `fieldInput`, …) are valid alongside `computed`.
248
245
 
249
246
  ::: warning
250
- Calculated columns are not available on `kind: "custom"` resources — they run raw SQL against a real table.
247
+ Computed columns are not available on `kind: "custom"` resources — they run raw SQL against a real table.
251
248
  Compute the value in `findAll` instead.
252
249
  :::
253
250
 
251
+ ::: warning Deprecated: `calculatedColumns`
252
+ The top-level `calculatedColumns` array is removed. Run `crouton migrate` to move each entry to `columns[id].computed`.
253
+ :::
254
+
254
255
  ## Includes
255
256
 
256
- Eagerly load relations with the list/detail queries:
257
+ Eagerly load relations with the list/detail queries. Declare them per operation so `findAll` and `findOne` can differ:
257
258
 
258
259
  ```json
259
260
  {
260
- "include": [
261
- "author",
262
- {
263
- "relation": "chapters",
264
- "orderBy": { "position": "asc" },
265
- "include": ["sections"]
261
+ "operations": {
262
+ "findAll": {
263
+ "include": [
264
+ "author",
265
+ { "relation": "chapters", "orderBy": { "position": "asc" }, "include": ["sections"] }
266
+ ]
267
+ },
268
+ "findOne": {
269
+ "include": [
270
+ "author",
271
+ { "relation": "chapters", "orderBy": { "position": "asc" }, "include": ["sections"] }
272
+ ]
266
273
  }
267
- ]
274
+ }
268
275
  }
269
276
  ```
270
277
 
278
+ When `findOne.include` is absent, crouton falls back to `findAll.include`.
279
+
271
280
  | Field | Type | Description |
272
281
  |------------|------------------|--------------------------------------------------------------------|
273
282
  | `relation` | `string` | Prisma relation field name |
@@ -276,3 +285,7 @@ Eagerly load relations with the list/detail queries:
276
285
 
277
286
  When a relation column has a `sort` option (see [Relations](relations.md#sorting-related-records)), the loader
278
287
  automatically injects the corresponding `orderBy` into the include clause — no manual configuration needed.
288
+
289
+ ::: warning Deprecated: root `include`
290
+ The top-level `include` field is removed. Run `crouton migrate` to move it to `operations.findAll.include` / `operations.findOne.include`.
291
+ :::
@@ -231,6 +231,7 @@ The table node carries all table-specific settings:
231
231
  "createdAt",
232
232
  "updatedAt"
233
233
  ],
234
+ "searchFields": ["title", "isbn"],
234
235
  "filterFields": ["status"],
235
236
  "rowActions": [
236
237
  { "id": "approve", "label": "Approve", "type": "post" }
@@ -244,28 +245,63 @@ The table node carries all table-specific settings:
244
245
  | Field | Description |
245
246
  |---|---|
246
247
  | `items[]` | Column order + per-column overrides. Entries are field ids (strings) or field controls (objects with `id`). No nested nodes — the table is flat. |
248
+ | `searchFields` | Column ids included in `?q=` free-text search. |
247
249
  | `filterFields` | Column ids that get a filter control in the toolbar. |
248
250
  | `rowActions` | Per-record actions (receive the record id). |
249
251
  | `tableActions` | Global toolbar actions (no record id). |
250
252
  | `title` / `label` | Heading rendered above the table. `title` wins over `label`. |
251
253
 
252
- ::: tip Free-text search
253
- Free-text search (`?q=`) is declared on the operation, not the table layout. See [`operations.findAll.search`](operations.md#free-text-search).
254
+ #### Per-column table item options
255
+
256
+ | Field | Type | Description |
257
+ |---|---|---|
258
+ | `sortable` | boolean | Enable sort toggle on this column header. |
259
+ | `defaultSort` | `true` \| `"asc"` \| `"desc"` | Initial sort column and direction. `true` = `"asc"`. At most one column. |
260
+ | `width` | named (`"xs"` – `"xl"`) or CSS value | Column width. |
261
+ | `minWidth` / `maxWidth` | CSS value | Width constraints. |
262
+ | `align` | `"left"` \| `"center"` \| `"right"` | Cell text alignment. Number columns default to `"right"`. |
263
+ | `truncate` | `boolean` \| number | `true` = single-line ellipsis; number = max lines before clamp. |
264
+ | `pin` | `"left"` \| `"right"` | Sticky column. |
265
+ | `visible` | boolean | `false` = hidden by default (still in the schema; no column chooser yet). |
266
+ | `headerTooltip` | string | Tooltip text on the column header. |
267
+ | `showWhen` | `{ field, eq?, neq?, exists?, notExists? }` | Show column when condition matches. |
268
+ | `hideWhen` | Same shape | Hide column when condition matches. |
269
+ | `disabledWhen` | Same shape | Disable column when condition matches. |
270
+
271
+ `type` + `options` override the cell renderer for this column in the table only.
272
+
273
+ ::: tip
274
+ `colspan`, `rowspan`, and `hideLabel` are form-layout options and have no effect on table items — they produce a validation error if set.
254
275
  :::
255
276
 
256
- #### Per-column table item options
277
+ #### Table node options
257
278
 
258
- Beyond the standard `colspan`/`label`/`type`/`options` overrides, table items also accept:
279
+ Beyond `items`, the table node accepts presentation options:
259
280
 
260
- | Field | Description |
261
- |---|---|
262
- | `sortable` | `true` to enable sort toggle on this column header. |
263
- | `defaultSort` | `true` to sort by this column on initial load. At most one column. |
264
- | `showWhen` | `{ field, eq?, neq?, exists?, notExists? }` — show column when condition matches. |
265
- | `hideWhen` | Same shape — hide column when condition matches. |
266
- | `disabledWhen` | Same shape — disable column when condition matches. |
281
+ | Field | Type | Description |
282
+ |---|---|---|
283
+ | `pageSize` | number | Default page size (default `20`). Frontend hint; server still caps it. |
284
+ | `pageSizes` | number[] | Page size selector options (e.g. `[20, 50, 100]`). |
285
+ | `density` | `"normal"` \| `"compact"` | Row height. |
286
+ | `selectable` | boolean | Show row checkboxes for multi-select. |
287
+ | `rowClick` | `"view"` \| `"edit"` \| `"none"` | What happens when a row is clicked. |
288
+ | `emptyText` | string | Message shown when there are no rows. |
267
289
 
268
- `type` + `options` override the cell renderer for this column in the table only.
290
+ ```json
291
+ "table": {
292
+ "pageSize": 50,
293
+ "pageSizes": [20, 50, 100],
294
+ "density": "compact",
295
+ "rowClick": "view",
296
+ "emptyText": "No books yet",
297
+ "items": [
298
+ { "id": "title", "sortable": true, "defaultSort": "desc", "width": "md", "truncate": 2 },
299
+ { "id": "price", "align": "right", "width": "xs" },
300
+ { "id": "status", "pin": "left" },
301
+ { "id": "internalRef", "visible": false }
302
+ ]
303
+ }
304
+ ```
269
305
 
270
306
  ### Table column order
271
307
 
@@ -275,7 +311,7 @@ Beyond the standard `colspan`/`label`/`type`/`options` overrides, table items al
275
311
  }
276
312
  ```
277
313
 
278
- The `items` list is the complete set of columns for the table. Only columns listed here are rendered; unlisted columns are hidden regardless of their `hiddenInTable` flag.
314
+ Columns present in the table but not listed here are appended in their original order.
279
315
 
280
316
  <LayoutDemo example="table-order" />
281
317
 
@@ -293,6 +329,7 @@ A `layout.table` with an outer container type (not `"table"`) wraps the table. P
293
329
  "type": "table",
294
330
  "title": "Books",
295
331
  "items": ["title", "author", { "id": "isbn", "sortable": true }, "publishedYear", "status"],
332
+ "searchFields": ["title", "isbn"],
296
333
  "filterFields": ["status"],
297
334
  "tableActions": [
298
335
  { "id": "searchOpenLibrary", "type": "link", "href": "https://openlibrary.org/search", "label": "Search Open Library" }
@@ -305,7 +342,7 @@ A `layout.table` with an outer container type (not `"table"`) wraps the table. P
305
342
  }
306
343
  ```
307
344
 
308
- All table settings (`rowActions`, `tableActions`, `filterFields`, `title`) go on the **table node**, not the wrapper.
345
+ All table settings (`rowActions`, `tableActions`, `searchFields`, `filterFields`, `title`) go on the **table node**, not the wrapper.
309
346
 
310
347
  Wrappers can nest arbitrarily:
311
348
 
@@ -334,26 +371,49 @@ Wrappers can nest arbitrarily:
334
371
  Use the wrapper pattern when you need a custom action component alongside the table — for example a barcode scanner that creates a record directly. Each component receives a `resource` prop (the `UseResource` instance for the current table).
335
372
  :::
336
373
 
337
- ## The layout is the full list
374
+ ## How a record opens (`layout.<form|view>.container`)
338
375
 
339
- When `layout.<ctx>` is defined, it is the **only** source of truth for that context. The layout lists every column that should be rendered — columns not listed are hidden, regardless of their `hiddenIn*` flag.
376
+ The `container` key on `layout.form` and `layout.view` controls how a record opens when a row is clicked.
340
377
 
341
- - **Unknown id** in a layout → validation error at load time (prevents silent typos).
342
- - **Unlisted columns** → not rendered (use the codemod below to migrate old files).
343
- - **Listed column with `hiddenIn<Ctx>: true`** → rendered (the layout wins; the flag is ignored).
378
+ ```json
379
+ "layout": {
380
+ "form": {
381
+ "container": { "mode": "modal", "size": "xl" },
382
+ "items": [...]
383
+ },
384
+ "view": {
385
+ "container": { "mode": "page" }
386
+ },
387
+ "table": {
388
+ "rowClick": "view",
389
+ "items": [...]
390
+ }
391
+ }
392
+ ```
344
393
 
345
- This applies independently per context: a layout for `form` does not affect `table` or `view`.
394
+ | Field | Values | Default | Description |
395
+ |---|---|---|---|
396
+ | `container.mode` | `"modal"` \| `"page"` | `"modal"` | How the record opens |
397
+ | `container.size` | `"xs"` \| `"sm"` \| `"lg"` \| `"xl"` | `"sm"` | Modal width (ignored for `"page"` mode) |
346
398
 
347
- ### Migrating existing files
399
+ `layout.table.rowClick` decides **what** opens (`"view"`, `"edit"`, or `"none"`); the container decides **how**.
348
400
 
349
- If you have resource files that relied on unlisted visible columns being appended automatically, run:
401
+ `view.component` replaces the whole detail view with a custom Vue component:
350
402
 
351
- ```sh
352
- crouton migrate --dry-run # preview changes
353
- crouton migrate # apply
403
+ ```json
404
+ "view": { "component": "BookDetail" }
354
405
  ```
355
406
 
356
- The codemod appends visible unlisted columns to the layout and removes hidden listed columns.
407
+ ::: tip Migration
408
+ The old root keys `display` and `modalSize` are removed. Run `crouton migrate` to move them automatically.
409
+ :::
410
+
411
+ ## Missing / extra columns
412
+
413
+ - A control id that doesn't match a visible column → **skipped, dev warning logged**.
414
+ - Visible columns not referenced anywhere in the layout → **appended at the end, dev warning logged** (nothing silently disappears).
415
+
416
+ <LayoutDemo example="missing-extra" />
357
417
 
358
418
  ## Playground
359
419
 
@@ -20,13 +20,16 @@ Each key accepts three forms:
20
20
  | `false` | Endpoint disabled — not registered |
21
21
  | `{ "uri": "/path/{id}" }` | **External** — client calls the given route directly; crouton registers nothing |
22
22
 
23
- `findAll` additionally accepts an object form to configure search and filter:
23
+ `findAll` and `findOne` additionally accept an object form to configure search, filter, and includes:
24
24
 
25
25
  | Value | Meaning |
26
26
  |-------|---------|
27
27
  | `{ "search": ["field1", "field2"] }` | Enable findAll with explicit free-text search fields |
28
28
  | `{ "filter": ["field1"] }` | Enable findAll with explicit filter fields (overrides per-column `filterable`) |
29
29
  | `{ "search": [...], "filter": [...] }` | Both |
30
+ | `{ "include": [...] }` | Eagerly load relations for this operation (see [Includes](columns.md#includes)) |
31
+
32
+ When `findOne.include` is absent, crouton falls back to `findAll.include`.
30
33
 
31
34
  ## Free-text search
32
35
 
@@ -1,87 +1,135 @@
1
1
  # Relations
2
2
 
3
- A relation is configured as a column with `fieldInput.format: "relation"` pointing to a sub-resource file:
3
+ A relation is configured on the **column** via `column.relation`. The `fieldInput` only picks and configures the UI widget.
4
4
 
5
5
  ```json
6
6
  {
7
- "column": "author_id",
8
- "fieldInput": {
9
- "format": "relation",
10
- "relationType": "manyToOne",
11
- "resource": "./author.resource"
7
+ "columns": {
8
+ "authorId": {
9
+ "label": "Author",
10
+ "relation": {
11
+ "resource": "./author/resource.json",
12
+ "type": "manyToOne",
13
+ "foreignKey": "authorId",
14
+ "displayKey": "name"
15
+ },
16
+ "fieldInput": { "type": "autocomplete" }
17
+ }
12
18
  }
13
19
  }
14
20
  ```
15
21
 
16
- `author.resource.json` describes the related resource (its columns, operations, and lookup display). Supported
17
- `relationType` values: `oneToOne`, `manyToOne`, `oneToMany`, `manyToMany`.
22
+ ::: tip Migration
23
+ `fieldInput.format: "relation"`, `fieldInput.relationType`, `fieldInput.resource`, and `fieldInput.foreignKey` are removed. Run `crouton migrate` to move them to `column.relation` automatically.
24
+ :::
25
+
26
+ ## column.relation reference
18
27
 
19
- The frontend picks the matching control automatically — an autocomplete for `manyToOne`, an editable nested table for
20
- `oneToMany`, and so on.
28
+ | Field | Description |
29
+ |---|---|
30
+ | `resource` | Relative path to the target `resource.json` |
31
+ | `type` | `"manyToOne"` \| `"oneToMany"` \| `"manyToMany"` |
32
+ | `foreignKey` | FK column name (on **this** resource for manyToOne; on **target** for oneToMany) |
33
+ | `name` | Prisma relation field name when different from the column id |
34
+ | `displayKey` | Field on the related record shown in autocomplete / lookup |
35
+ | `sort` | Default sort field for the related list |
36
+ | `sortDir` | `"asc"` \| `"desc"` (default `"asc"`) |
21
37
 
22
- ## Foreign key
38
+ ## manyToOne (autocomplete lookup)
23
39
 
24
- By default, the foreign key on the child model is derived as `${parentModel}Id` (camelCase), matching the standard
25
- Prisma convention. If the FK field in your Prisma schema uses a different name, set `foreignKey` on `fieldInput`:
40
+ FK is on **this** resource (e.g. `book.authorId → author.id`).
41
+ Frontend renders an autocomplete search dropdown.
26
42
 
27
43
  ```json
28
44
  {
29
- "column": "section",
30
- "fieldInput": {
31
- "format": "relation",
32
- "relationType": "oneToMany",
33
- "resource": "../section/resource.json",
34
- "foreignKey": "work_id"
45
+ "columns": {
46
+ "authorId": {
47
+ "label": "Author",
48
+ "relation": {
49
+ "resource": "./author/resource.json",
50
+ "type": "manyToOne",
51
+ "foreignKey": "authorId",
52
+ "displayKey": "name"
53
+ },
54
+ "fieldInput": { "type": "autocomplete" }
55
+ }
56
+ },
57
+ "operations": {
58
+ "findAll": { "include": ["author"] },
59
+ "findOne": { "include": ["author"] }
35
60
  }
36
61
  }
37
62
  ```
38
63
 
39
- When omitted, a parent model named `work` produces `foreignKey: "workId"`.
64
+ ## oneToMany (embedded panel)
40
65
 
41
- ## Relation options
66
+ FK is on the **target** resource (e.g. `review.bookId → book.id`).
67
+ Frontend renders an embedded table panel inside the detail view.
42
68
 
43
- `fieldInput.options` accepts the following fields for relation columns:
44
-
45
- | Option | Type | Description |
46
- |---------------------|---------------------|-------------------------------------------------------------------------|
47
- | `displayKey` | `string` | Field used as the label in the relation control (e.g. `"title"`) |
48
- | `direction` | `'row' \| 'column'` | CSS flex direction for the relation button layout |
49
- | `sort` | `string` | Field to sort related records by, e.g. `"title"` or `"author.name"` |
50
- | `sortDir` | `'asc' \| 'desc'` | Sort direction (default `"asc"`) |
51
- | `customComponent` | `string` | Name of a custom Vue component to override the relation control |
52
-
53
- ## Sorting related records
69
+ ```json
70
+ {
71
+ "columns": {
72
+ "reviews": {
73
+ "label": "Reviews",
74
+ "hiddenInTable": true,
75
+ "hiddenInForm": true,
76
+ "relation": {
77
+ "resource": "./review/resource.json",
78
+ "type": "oneToMany",
79
+ "foreignKey": "bookId",
80
+ "sort": "createdAt",
81
+ "sortDir": "desc"
82
+ },
83
+ "fieldInput": { "type": "relation" }
84
+ }
85
+ },
86
+ "operations": {
87
+ "findOne": { "include": ["reviews"] }
88
+ }
89
+ }
90
+ ```
54
91
 
55
- Add `sort` (and optionally `sortDir`) to `fieldInput.options` to control the order of related records. This affects:
92
+ Load oneToMany relations in `findOne.include` only — they are heavy and not needed in the list.
56
93
 
57
- - **Backend includes** — when the parent record is fetched, the included relation records are returned in the specified
58
- order (Prisma `orderBy` inside the `include` clause).
59
- - **Frontend picker** — when the relation control fetches its option list, the same sort params are forwarded as query
60
- parameters.
94
+ ## manyToMany
61
95
 
62
96
  ```json
63
97
  {
64
- "id": "sections",
65
- "label": "Sections",
66
- "hiddenInForm": true,
67
- "fieldInput": {
68
- "format": "relation",
69
- "resource": "./section/resource.json",
70
- "options": {
71
- "sort": "title",
72
- "sortDir": "asc",
73
- "displayKey": "title"
98
+ "columns": {
99
+ "tags": {
100
+ "label": "Tags",
101
+ "relation": {
102
+ "resource": "./tag/resource.json",
103
+ "type": "manyToMany",
104
+ "foreignKey": "bookId",
105
+ "name": "tags"
106
+ },
107
+ "fieldInput": { "type": "relation" }
74
108
  }
75
109
  }
76
110
  }
77
111
  ```
78
112
 
79
- Dotted paths work too — `"sort": "author.name"` sorts by a nested field.
113
+ ## Loading relations: operations.include
114
+
115
+ Root `include` is removed. Set includes per operation:
116
+
117
+ ```json
118
+ "operations": {
119
+ "findAll": { "include": ["author"] },
120
+ "findOne": { "include": ["author", { "relation": "chapters", "orderBy": { "position": "asc" } }] }
121
+ }
122
+ ```
123
+
124
+ The object form allows an `orderBy` clause when the relation's default sort isn't enough.
125
+
126
+ ## Detecting a relation column
127
+
128
+ Use `isRelation(col)` (exported from `@ghentcdh/crouton-core`) — returns `true` when `col.relation` is set.
80
129
 
81
130
  ## `parent.idType`
82
131
 
83
- When a resource is mounted under a parent via `parent.param`, the parent's id type defaults to `"number"`. If the
84
- parent uses string/UUID ids, declare it explicitly:
132
+ When a resource is mounted under a parent via `parent.param`, the parent's id type defaults to `"number"`. If the parent uses string/UUID ids, declare it explicitly:
85
133
 
86
134
  ```json
87
135
  {
@@ -93,62 +141,69 @@ parent uses string/UUID ids, declare it explicitly:
93
141
 
94
142
  ## Custom relation control
95
143
 
96
- Register a custom Vue component in the `customComponents` registry and reference it from `fieldInput.options`:
144
+ Register a custom Vue component and reference it from `fieldInput.options`:
97
145
 
98
146
  ```json
99
147
  {
100
- "column": "sections",
101
- "fieldInput": {
102
- "format": "relation",
103
- "resource": "../section/resource.json",
104
- "options": {
105
- "customComponent": "work-sections",
106
- "sort": "section_number",
107
- "displayKey": "title"
148
+ "columns": {
149
+ "sections": {
150
+ "relation": {
151
+ "resource": "../section/resource.json",
152
+ "type": "oneToMany",
153
+ "foreignKey": "workId",
154
+ "sort": "sectionNumber",
155
+ "displayKey": "title"
156
+ },
157
+ "fieldInput": {
158
+ "type": "relation",
159
+ "options": { "customComponent": "work-sections" }
160
+ }
108
161
  }
109
162
  }
110
163
  }
111
164
  ```
112
165
 
113
166
  The custom component receives `wrapper`, `value` (v-model), `appliedOptions`, `schema`, and `uischema` as props.
114
- All `fieldInput.options` are available via `appliedOptions`.
115
167
 
116
168
  See [Custom renderers](../4.frontend/custom-renderers.md) for how to register the component.
117
169
 
118
170
  ## Complete example
119
171
 
120
- A full `resource.json` showing manyToOne (autocomplete) and manyToMany (embedded table) relations together:
121
-
122
172
  ```json
123
173
  {
124
174
  "$schema": "https://ghentcdh.github.io/crouton/schema/v1/resource.schema.json",
125
175
  "schemaVersion": 1,
126
176
  "name": "book",
127
- "model": "Book",
128
177
  "title": "Books",
178
+ "source": { "model": "Book" },
129
179
  "columns": {
130
180
  "id": { "idField": true, "hiddenInForm": true },
131
- "title": { "searchable": true, "filterable": true, "sortable": true },
181
+ "title": {},
132
182
  "authorId": {
133
183
  "label": "Author",
134
- "searchable": true,
135
- "fieldInput": {
136
- "type": "autocomplete",
137
- "relationType": "manyToOne",
138
- "resource": "./author.resource"
139
- }
184
+ "relation": {
185
+ "resource": "./author/resource.json",
186
+ "type": "manyToOne",
187
+ "foreignKey": "authorId",
188
+ "displayKey": "name"
189
+ },
190
+ "fieldInput": { "type": "autocomplete" }
140
191
  },
141
192
  "categories": {
142
193
  "label": "Categories",
143
194
  "hiddenInTable": true,
144
- "fieldInput": {
145
- "format": "relation",
146
- "relationType": "manyToMany",
147
- "resource": "./category.resource"
148
- }
195
+ "relation": {
196
+ "resource": "./category/resource.json",
197
+ "type": "manyToMany",
198
+ "foreignKey": "bookId"
199
+ },
200
+ "fieldInput": { "type": "relation" }
149
201
  }
150
202
  },
151
- "include": ["author", "categories"]
203
+ "operations": {
204
+ "findAll": { "search": ["title"], "include": ["author"] },
205
+ "findOne": { "include": ["author", "categories"] }
206
+ }
152
207
  }
153
208
  ```
154
209
 
@@ -64,6 +64,20 @@ Switching themes at runtime works the daisyUI way, with a `data-theme` attribute
64
64
  <html data-theme="mytheme"></html>
65
65
  ```
66
66
 
67
+ ## Column width scale
68
+
69
+ Table items and form items share a named width scale. Use these tokens in `layout.table` items (`width` field) or form items (`colspan`-equivalent):
70
+
71
+ | Token | Approximate width |
72
+ |---|---|
73
+ | `xs` | ~4 rem |
74
+ | `sm` | ~8 rem |
75
+ | `md` | ~12 rem |
76
+ | `lg` | ~18 rem |
77
+ | `xl` | ~24 rem |
78
+
79
+ You can also pass any CSS value directly (e.g. `"120px"`, `"8rem"`, `"20%"`).
80
+
67
81
  ## Targeted overrides
68
82
 
69
83
  The components use regular daisyUI class names (`btn`, `modal`, `table`, `drawer`, `alert`, …), so narrow tweaks are plain CSS: