@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.
- package/agent-docs/1.getting-started/manual-setup.md +3 -0
- package/agent-docs/2.resources/columns.md +43 -30
- package/agent-docs/2.resources/layout.md +86 -26
- package/agent-docs/2.resources/operations.md +4 -1
- package/agent-docs/2.resources/relations.md +131 -76
- package/agent-docs/4.frontend/styling.md +14 -0
- package/agent-docs/4.frontend/use-crouton.md +27 -0
- package/dist/crouton.schema.json +82 -0
- package/dist/index.cjs +964 -690
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1735 -549
- package/dist/index.d.ts +1735 -549
- package/dist/index.js +950 -688
- package/dist/index.js.map +1 -1
- package/dist/resource.schema.json +495 -332
- package/dist/resource.schema.v1.json +495 -332
- package/package.json +1 -1
|
@@ -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`
|
|
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`
|
|
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
|
-
##
|
|
221
|
+
## Computed columns
|
|
221
222
|
|
|
222
|
-
Read-only columns
|
|
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
|
-
"
|
|
227
|
-
{
|
|
228
|
-
"id": "chapter_count",
|
|
229
|
-
"alias": "chapter_count",
|
|
227
|
+
"columns": {
|
|
228
|
+
"chapter_count": {
|
|
230
229
|
"label": "Chapters",
|
|
231
|
-
"
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
240
|
-
|
|
241
|
-
| `
|
|
242
|
-
| `
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
261
|
-
"
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
####
|
|
277
|
+
#### Table node options
|
|
257
278
|
|
|
258
|
-
Beyond
|
|
279
|
+
Beyond `items`, the table node accepts presentation options:
|
|
259
280
|
|
|
260
|
-
| Field | Description |
|
|
261
|
-
|
|
262
|
-
| `
|
|
263
|
-
| `
|
|
264
|
-
| `
|
|
265
|
-
| `
|
|
266
|
-
| `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
374
|
+
## How a record opens (`layout.<form|view>.container`)
|
|
338
375
|
|
|
339
|
-
|
|
376
|
+
The `container` key on `layout.form` and `layout.view` controls how a record opens when a row is clicked.
|
|
340
377
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
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
|
-
|
|
399
|
+
`layout.table.rowClick` decides **what** opens (`"view"`, `"edit"`, or `"none"`); the container decides **how**.
|
|
348
400
|
|
|
349
|
-
|
|
401
|
+
`view.component` replaces the whole detail view with a custom Vue component:
|
|
350
402
|
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
crouton migrate # apply
|
|
403
|
+
```json
|
|
404
|
+
"view": { "component": "BookDetail" }
|
|
354
405
|
```
|
|
355
406
|
|
|
356
|
-
|
|
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
|
|
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
|
|
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
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
17
|
-
`
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
##
|
|
38
|
+
## manyToOne (autocomplete lookup)
|
|
23
39
|
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
64
|
+
## oneToMany (embedded panel)
|
|
40
65
|
|
|
41
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
92
|
+
Load oneToMany relations in `findOne.include` only — they are heavy and not needed in the list.
|
|
56
93
|
|
|
57
|
-
|
|
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
|
-
"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"
|
|
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
|
-
|
|
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
|
|
144
|
+
Register a custom Vue component and reference it from `fieldInput.options`:
|
|
97
145
|
|
|
98
146
|
```json
|
|
99
147
|
{
|
|
100
|
-
"
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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": {
|
|
181
|
+
"title": {},
|
|
132
182
|
"authorId": {
|
|
133
183
|
"label": "Author",
|
|
134
|
-
"
|
|
135
|
-
|
|
136
|
-
"type": "
|
|
137
|
-
"
|
|
138
|
-
"
|
|
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
|
-
"
|
|
145
|
-
"
|
|
146
|
-
"
|
|
147
|
-
"
|
|
148
|
-
}
|
|
195
|
+
"relation": {
|
|
196
|
+
"resource": "./category/resource.json",
|
|
197
|
+
"type": "manyToMany",
|
|
198
|
+
"foreignKey": "bookId"
|
|
199
|
+
},
|
|
200
|
+
"fieldInput": { "type": "relation" }
|
|
149
201
|
}
|
|
150
202
|
},
|
|
151
|
-
"
|
|
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:
|