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

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.
Files changed (57) hide show
  1. package/agent-docs/1.getting-started/index.md +45 -41
  2. package/agent-docs/2.resources/actions.md +40 -0
  3. package/agent-docs/2.resources/columns.md +126 -50
  4. package/agent-docs/2.resources/field-inputs/autocomplete.md +26 -0
  5. package/agent-docs/2.resources/layout.md +329 -119
  6. package/agent-docs/2.resources/operations.md +1 -5
  7. package/agent-docs/2.resources/relations.md +14 -11
  8. package/agent-docs/3.backend/authentication.md +156 -0
  9. package/agent-docs/3.backend/data-sources.md +3 -3
  10. package/agent-docs/4.frontend/croutonform.md +8 -8
  11. package/agent-docs/4.frontend/translations.md +78 -0
  12. package/agent-docs/5.tooling/migrate.md +1 -100
  13. package/agent-docs/6.recipes/config-only-resource.md +1 -1
  14. package/agent-docs/README.md +1 -0
  15. package/agent-docs/reference/parse-schema.md +0 -2
  16. package/dist/crouton.schema.json +5 -0
  17. package/dist/field-input-schemas/array.field-input.schema.json +44 -0
  18. package/dist/field-input-schemas/array.field-input.schema.v1.json +44 -0
  19. package/dist/field-input-schemas/autocomplete.field-input.schema.json +296 -0
  20. package/dist/field-input-schemas/autocomplete.field-input.schema.v1.json +296 -0
  21. package/dist/field-input-schemas/boolean.field-input.schema.json +44 -0
  22. package/dist/field-input-schemas/boolean.field-input.schema.v1.json +44 -0
  23. package/dist/field-input-schemas/custom.field-input.schema.json +44 -0
  24. package/dist/field-input-schemas/custom.field-input.schema.v1.json +44 -0
  25. package/dist/field-input-schemas/date-range.field-input.schema.json +44 -0
  26. package/dist/field-input-schemas/date-range.field-input.schema.v1.json +44 -0
  27. package/dist/field-input-schemas/date.field-input.schema.json +44 -0
  28. package/dist/field-input-schemas/date.field-input.schema.v1.json +44 -0
  29. package/dist/field-input-schemas/markdown.field-input.schema.json +44 -0
  30. package/dist/field-input-schemas/markdown.field-input.schema.v1.json +44 -0
  31. package/dist/field-input-schemas/number.field-input.schema.json +44 -0
  32. package/dist/field-input-schemas/number.field-input.schema.v1.json +44 -0
  33. package/dist/field-input-schemas/relation.field-input.schema.json +44 -0
  34. package/dist/field-input-schemas/relation.field-input.schema.v1.json +44 -0
  35. package/dist/field-input-schemas/select.field-input.schema.json +44 -0
  36. package/dist/field-input-schemas/select.field-input.schema.v1.json +44 -0
  37. package/dist/field-input-schemas/string.field-input.schema.json +44 -0
  38. package/dist/field-input-schemas/string.field-input.schema.v1.json +44 -0
  39. package/dist/field-input-schemas/textarea.field-input.schema.json +44 -0
  40. package/dist/field-input-schemas/textarea.field-input.schema.v1.json +44 -0
  41. package/dist/field-input-schemas/toggle.field-input.schema.json +44 -0
  42. package/dist/field-input-schemas/toggle.field-input.schema.v1.json +44 -0
  43. package/dist/index.cjs +4223 -3090
  44. package/dist/index.cjs.map +1 -1
  45. package/dist/index.d.cts +8638 -6993
  46. package/dist/index.d.ts +8638 -6993
  47. package/dist/index.js +4082 -2935
  48. package/dist/index.js.map +1 -1
  49. package/dist/resource.schema.json +177 -45
  50. package/dist/resource.schema.v1.json +177 -45
  51. package/dist/testing/index.cjs +38 -0
  52. package/dist/testing/index.cjs.map +1 -0
  53. package/dist/testing/index.d.cts +20 -0
  54. package/dist/testing/index.d.ts +20 -0
  55. package/dist/testing/index.js +13 -0
  56. package/dist/testing/index.js.map +1 -0
  57. package/package.json +9 -1
@@ -10,7 +10,8 @@ Two ways to start with crouton: **create a new project** from scratch, or **add
10
10
 
11
11
  ## @ghentcdh/create-crouton
12
12
 
13
- Scaffold a complete crouton project — a NestJS backend with Prisma, pre-configured datasource, Docker Compose, and optionally an Nx monorepo layout with a Vue frontend.
13
+ Scaffold a complete crouton project — a NestJS backend with Prisma, pre-configured datasource, Docker Compose, and
14
+ optionally an Nx monorepo layout with a Vue frontend.
14
15
 
15
16
  ```sh
16
17
  npm create @ghentcdh/crouton my-app
@@ -40,21 +41,21 @@ node packages/create-crouton/dist/index.js my-test-app
40
41
 
41
42
  ### Flags
42
43
 
43
- | Flag | Default | Description |
44
- | --- | --- | --- |
45
- | `--nx` | prompt | Use Nx monorepo layout (backend + frontend + shared libs). |
46
- | `--no-nx` | — | Skip Nx layout (scaffold a regular single-app project). |
47
- | `--no-frontend` | — | Skip Vue frontend generation (Nx layout only). |
48
- | `--no-postgres` | — | Skip PostgreSQL setup (Prisma schema, adapter, Docker). |
49
- | `--sample` | — | Include a sample resource. |
50
- | `--pm <manager>` | prompt | Package manager: `pnpm`, `npm`, `yarn`, or `bun`. |
51
- | `--no-install` | — | Skip dependency installation. |
52
- | `--no-git` | — | Skip `git init`. |
53
- | `--no-docker` | — | Skip Docker files (Dockerfile, compose.yml). |
54
- | `--prefix <name>` | prompt | Subfolder prefix for apps/config (e.g. `split`). Implies Nx layout. |
55
- | `--db-url <url>` | prompt | Database connection URL. |
56
- | `-y, --yes` | — | Accept all defaults (non-interactive). |
57
- | `--force` | — | Overwrite existing files. |
44
+ | Flag | Default | Description |
45
+ |-------------------|---------|---------------------------------------------------------------------|
46
+ | `--nx` | prompt | Use Nx monorepo layout (backend + frontend + shared libs). |
47
+ | `--no-nx` | — | Skip Nx layout (scaffold a regular single-app project). |
48
+ | `--no-frontend` | — | Skip Vue frontend generation (Nx layout only). |
49
+ | `--no-postgres` | — | Skip PostgreSQL setup (Prisma schema, adapter, Docker). |
50
+ | `--sample` | — | Include a sample resource. |
51
+ | `--pm <manager>` | prompt | Package manager: `pnpm`, `npm`, `yarn`, or `bun`. |
52
+ | `--no-install` | — | Skip dependency installation. |
53
+ | `--no-git` | — | Skip `git init`. |
54
+ | `--no-docker` | — | Skip Docker files (Dockerfile, compose.yml). |
55
+ | `--prefix <name>` | prompt | Subfolder prefix for apps/config (e.g. `split`). Implies Nx layout. |
56
+ | `--db-url <url>` | prompt | Database connection URL. |
57
+ | `-y, --yes` | — | Accept all defaults (non-interactive). |
58
+ | `--force` | — | Overwrite existing files. |
58
59
 
59
60
  ### Layouts
60
61
 
@@ -113,7 +114,8 @@ my-app/
113
114
 
114
115
  #### Nx monorepo with prefix
115
116
 
116
- When using `--prefix split`, apps and generated code live under a subfolder. This is useful when the Nx workspace also contains non-crouton projects.
117
+ When using `--prefix split`, apps and generated code live under a subfolder. This is useful when the Nx workspace also
118
+ contains non-crouton projects.
117
119
 
118
120
  ```
119
121
  my-app/
@@ -138,7 +140,8 @@ The CLI runs these steps automatically (unless skipped via flags):
138
140
 
139
141
  1. **`git init`** — initialises a repository with an initial commit.
140
142
  2. **`<pm> install`** — installs all dependencies.
141
- 3. **`crouton update resources`** — introspects the database and generates resource CRUD (best-effort; warns on failure if DB is not running).
143
+ 3. **`crouton update resources`** — introspects the database and generates resource CRUD (best-effort; warns on failure
144
+ if DB is not running).
142
145
 
143
146
  Then follow the printed next-steps:
144
147
 
@@ -157,7 +160,8 @@ crouton update resources --prefix split
157
160
 
158
161
  ## @ghentcdh/add-crouton
159
162
 
160
- Add crouton to an **existing** NestJS project — regular or Nx. This command is additive: it never overwrites existing files.
163
+ Add crouton to an **existing** NestJS project — regular or Nx. This command is additive: it never overwrites existing
164
+ files.
161
165
 
162
166
  ```sh
163
167
  npx @ghentcdh/add-crouton
@@ -167,32 +171,32 @@ npx @ghentcdh/add-crouton --cwd /path/to/project
167
171
 
168
172
  ### Flags
169
173
 
170
- | Flag | Default | Description |
171
- | --- | --- | --- |
172
- | `--cwd <dir>` | `.` | Project directory. |
173
- | `--backend <app>` | auto-detect | Backend app name (Nx only). |
174
- | `--frontend <app>` | auto-detect | Frontend app name (Nx only). |
175
- | `--no-frontend` | — | Skip frontend detection. |
176
- | `--pm <manager>` | auto-detect | Package manager. |
177
- | `--no-install` | — | Skip dependency installation. |
178
- | `--no-docker` | — | Skip Docker file generation. |
179
- | `-y, --yes` | — | Accept all defaults (non-interactive). |
174
+ | Flag | Default | Description |
175
+ |--------------------|-------------|----------------------------------------|
176
+ | `--cwd <dir>` | `.` | Project directory. |
177
+ | `--backend <app>` | auto-detect | Backend app name (Nx only). |
178
+ | `--frontend <app>` | auto-detect | Frontend app name (Nx only). |
179
+ | `--no-frontend` | — | Skip frontend detection. |
180
+ | `--pm <manager>` | auto-detect | Package manager. |
181
+ | `--no-install` | — | Skip dependency installation. |
182
+ | `--no-docker` | — | Skip Docker file generation. |
183
+ | `-y, --yes` | — | Accept all defaults (non-interactive). |
180
184
 
181
185
  ### What it does
182
186
 
183
- 1. **Detects project type** — checks for `nx.json` at or above the current directory to distinguish Nx workspaces from regular projects.
184
- 2. **Discovers apps** (Nx only) — scans `apps/` subdirs, classifies them as backend (`@nestjs/core` in deps) or frontend (`vue`/`vite` in deps), and prompts which to use.
187
+ 1. **Detects project type** — checks for `nx.json` at or above the current directory to distinguish Nx workspaces from
188
+ regular projects.
189
+ 2. **Discovers apps** (Nx only) — scans `apps/` subdirs, classifies them as backend (`@nestjs/core` in deps) or frontend
190
+ (`vue`/`vite` in deps), and prompts which to use.
185
191
  3. **Detects package manager** — from lockfile presence (pnpm-lock.yaml, package-lock.json, yarn.lock, bun.lockb).
186
- 4. **Scans dependencies** — reports missing backend deps (`@ghentcdh/crouton-api`, `@ghentcdh/crouton-core`, `prisma`, etc.).
192
+ 4. **Scans dependencies** — reports missing backend deps (`@ghentcdh/crouton-api`, `@ghentcdh/crouton-core`, `prisma`,
193
+ etc.).
187
194
  5. **Writes config files** (if absent):
188
- - `crouton.json` — with detected `resourcesDir` and `dataSourcesDir`
189
- - `crouton.enums.json` — empty enum registry
190
- - `data-sources/default/` — default datasource with Prisma schema and config
191
- - `.env.example` — with `DATABASE_URL` placeholder
195
+ - `crouton.json` — with detected `resourcesDir` and `dataSourcesDir`
196
+ - `crouton.enums.json` — empty enum registry
197
+ - `data-sources/default/` — default datasource with Prisma schema and config
198
+ - `.env.example` — with `DATABASE_URL` placeholder
192
199
  6. **Installs dependencies** — runs `<pm> install` if missing deps were detected.
193
200
 
194
- After running `@ghentcdh/add-crouton`, wire `CroutonApiModule` into your NestJS app module — see [Backend setup](backend.md).
195
-
196
- ## Keeping agent files current
197
-
198
- After upgrading crouton, run `crouton migrate` to update your `resource.json` files **and** refresh the Claude skill, Cursor rule, and `AGENTS.md` block to the new version. See [Migrations](../5.tooling/migrate.md#agent-files) for details.
201
+ After running `@ghentcdh/add-crouton`, wire `CroutonApiModule` into your NestJS app module —
202
+ see [Backend setup](backend.md).
@@ -141,3 +141,43 @@ The `condition` object takes a `field`, an operator, and (except for the existen
141
141
  ## Display options
142
142
 
143
143
  Table actions support `icon`, `label`, and `tooltip`; row actions support `label`. The HTTP `method` of a procedure action defaults to `post`.
144
+
145
+ ## Toolbar status feedback
146
+
147
+ When a procedure action runs, the CRUD toolbar shows an inline status message. No toasts are shown — feedback stays next to the table.
148
+
149
+ | State | Icon | Default text |
150
+ |-------|------|--------------|
151
+ | In progress | ⏳ | *Processing…* |
152
+ | Success | ✓ | Server `message`, or *Done* |
153
+ | Error | ✗ | Server `message`, or *Action failed* |
154
+ | Network error | ✗ | *Something went wrong, please try again* |
155
+
156
+ Override the default messages per action with optional fields on a procedure action:
157
+
158
+ ```json
159
+ {
160
+ "layout": {
161
+ "table": {
162
+ "rowActions": [
163
+ {
164
+ "id": "publish",
165
+ "label": "Publish",
166
+ "procedure": "publish",
167
+ "pendingMessage": "Publishing…",
168
+ "successMessage": "Published successfully",
169
+ "errorMessage": "Could not publish"
170
+ }
171
+ ]
172
+ }
173
+ }
174
+ }
175
+ ```
176
+
177
+ | Field | Purpose |
178
+ |-------|---------|
179
+ | `pendingMessage` | Shown immediately when the button is clicked. |
180
+ | `successMessage` | Shown on success. Falls back to the server `message`, then *Done*. |
181
+ | `errorMessage` | Shown when the server returns `success: false`. Falls back to the server `message`, then *Action failed*. |
182
+
183
+ Success messages auto-clear after 3 s; error messages stay for 5 s.
@@ -16,21 +16,21 @@
16
16
 
17
17
  Both forms support the same options:
18
18
 
19
- | Option | Description |
20
- |---------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
21
- | `idField` | Marks the id column |
22
- | `type` | Data type — a shorthand (`"string"`, `"integer"`, `"boolean"`, `"date"`, …) or a JSON Schema fragment for nested shapes. Optional on a prisma resource (derived from the Zod model); **required on every column of a `kind: "custom"` resource**, see [Custom resources](custom-resource.md#column-types) |
23
- | `label` / `hideLabel` | Display label, or hide it |
24
- | `hiddenInTable` / `hiddenInForm` / `hiddenInView` | Visibility per context when **no layout is defined for that context**. When `layout.<ctx>` exists it is the full list — only listed columns are rendered, and these flags are ignored. On a `oneToMany` relation column, `hiddenInTable` also drops it from the `_count` subquery `findAll` issues — that count only ever fed a table cell |
25
- | `createable` / `updateable` | Whether the field is written on create/update |
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
- | `displayKey` | Nested field to display (e.g. `author.name`) |
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
- | `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
- | `column` | Source column name when it differs from the key/`id` (defaults to `id`) |
31
- | `enum` | Name of a shared enum in `crouton.enums.json`; its `{ value, label }[]` is injected into `fieldInput.options.values` |
32
- | `extend` | Path to another `resource.json` whose columns are expanded as virtual sub-columns under this key (with a per-sub-column `columns` override map) |
33
- | `fieldInput` | Form control configuration, see below |
19
+ | Option | Description |
20
+ |---------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
21
+ | `idField` | Marks the id column |
22
+ | `type` | Data type — a shorthand (`"string"`, `"integer"`, `"boolean"`, `"date"`, …) or a JSON Schema fragment for nested shapes. Optional on a prisma resource (derived from the Zod model); **required on every column of a `kind: "custom"` resource**, see [Custom resources](custom-resource.md#column-types) |
23
+ | `label` / `hideLabel` | Display label, or hide it |
24
+ | `hiddenInTable` / `hiddenInForm` / `hiddenInView` | Visibility per context, used **only when that context has no `layout.<ctx>.items`** — they are the fallback that builds the layout. As soon as `layout.<ctx>` lists field items, those items are the full list: only listed columns render and these flags are ignored. On a `oneToMany` relation column, `hiddenInTable` (when acting as the table fallback) also drops it from the `_count` subquery `findAll` issues |
25
+ | `createable` / `updateable` | Whether the field is written on create/update |
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
+ | `displayKey` | Nested field to display (e.g. `author.name`) |
28
+ | `showInLookup` *(deprecated)* | **Use `lookup.labels` at the resource root instead**. Sets the display label when this resource appears as an autocomplete option in another resource. Does **not** affect `?q=` search. |
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
+ | `column` | Source column name when it differs from the key/`id` (defaults to `id`) |
31
+ | `enum` | Name of a shared enum in `crouton.enums.json`; its `{ value, label }[]` is injected into `fieldInput.options.values` |
32
+ | `extend` | Path to another `resource.json` whose columns are expanded as virtual sub-columns under this key (with a per-sub-column `columns` override map) |
33
+ | `fieldInput` | Form control configuration, see below |
34
34
 
35
35
  ::: warning Moved to `layout.table`
36
36
  The following column-level options are **deprecated** and will be removed. Use `layout.table` instead:
@@ -42,7 +42,8 @@ The following column-level options are **deprecated** and will be removed. Use `
42
42
  - `showWhen` / `hideWhen` / `disabledWhen` → `layout.table.items[{ id, showWhen, ... }]`
43
43
  - `fieldTable` → `layout.table.items[{ id, type, options }]`
44
44
 
45
- See [Layout — Table layout](layout.md#table-layout-layouttable) and [migrate-to-next-alpha.md](/migrate-to-next-alpha) for migration details.
45
+ See [Layout — Table layout](layout.md#table-layout-layouttable) and [migrate-to-next-alpha.md](/migrate-to-next-alpha)
46
+ for migration details.
46
47
  :::
47
48
 
48
49
  ## Free-text search (`?q=`)
@@ -53,16 +54,23 @@ Declare which columns are included in free-text search via `operations.findAll.s
53
54
  {
54
55
  "operations": {
55
56
  "findAll": {
56
- "search": ["title", "authorId"]
57
+ "search": [
58
+ "title",
59
+ "authorId"
60
+ ]
57
61
  }
58
62
  }
59
63
  }
60
64
  ```
61
65
 
62
- - **Multiple entries** are ORed together: `?q=tolkien` matches books whose `title` contains "tolkien" OR whose author name contains "tolkien".
63
- - **`manyToOne` relation columns** (e.g. `authorId`) automatically resolve to the related resource's first visible display field (e.g. `author.name`), so you never search against a raw foreign key string.
64
- - **`showInLookup`** is a separate concern — it controls the label shown when this resource appears as an autocomplete option somewhere else. Do not use it to drive `?q=` search; use `operations.findAll.search` instead.
65
- - **Default**: when `findAll: true` and no explicit `search` is declared, all simple (non-id, non-relation, non-date) columns are searched.
66
+ - **Multiple entries** are ORed together: `?q=tolkien` matches books whose `title` contains "tolkien" OR whose author
67
+ name contains "tolkien".
68
+ - **`manyToOne` relation columns** (e.g. `authorId`) automatically resolve to the related resource's first visible
69
+ display field (e.g. `author.name`), so you never search against a raw foreign key string.
70
+ - **`showInLookup`** is a separate concern — it controls the label shown when this resource appears as an autocomplete
71
+ option somewhere else. Do not use it to drive `?q=` search; use `operations.findAll.search` instead.
72
+ - **Default**: when `findAll: true` and no explicit `search` is declared, all simple (non-id, non-relation, non-date)
73
+ columns are searched.
66
74
 
67
75
  ## Nested object and array columns
68
76
 
@@ -75,15 +83,21 @@ A column's `type` may be a full JSON Schema fragment, which is how you describe
75
83
  "type": {
76
84
  "type": "object",
77
85
  "properties": {
78
- "id": { "type": "string" },
79
- "name": { "type": "string" }
86
+ "id": {
87
+ "type": "string"
88
+ },
89
+ "name": {
90
+ "type": "string"
91
+ }
80
92
  }
81
93
  }
82
94
  },
83
95
  "tags": {
84
96
  "type": {
85
97
  "type": "array",
86
- "items": { "type": "string" }
98
+ "items": {
99
+ "type": "string"
100
+ }
87
101
  }
88
102
  }
89
103
  }
@@ -129,10 +143,10 @@ form-open time, not persisted until the user saves.
129
143
 
130
144
  #### Dynamic default tokens
131
145
 
132
- | Token | Resolves to |
133
- |------------|----------------------------------------------------------|
134
- | `"$now"` | Current datetime as an ISO 8601 string |
135
- | `"$today"` | Current date as an ISO 8601 date string (`"2025-03-14"`) |
146
+ | Token | Resolves to |
147
+ |------------|---------------------------------------------------------------------|
148
+ | `"$now"` | Current datetime as an ISO 8601 string |
149
+ | `"$today"` | Current date as an ISO 8601 date string (`"2025-03-14"`) |
136
150
  | `"$user"` | Current user object (requires `defaults` in `CroutonPlugin` config) |
137
151
 
138
152
  **Using `$user`** requires passing the current user when setting up the plugin:
@@ -163,7 +177,8 @@ onMounted(async () => {
163
177
 
164
178
  ## Field variants — per-context overrides
165
179
 
166
- A single `fieldInput` drives the form, the read-only view, and the table cell. To render differently in a specific context, use layout item overrides:
180
+ A single `fieldInput` drives the form, the read-only view, and the table cell. To render differently in a specific
181
+ context, use layout item overrides:
167
182
 
168
183
  - **form** ← `fieldInput`
169
184
  - **view** ← `fieldInput` (override per-column via `layout.view.items[{ id, type, options }]`)
@@ -178,19 +193,32 @@ A single `fieldInput` drives the form, the read-only view, and the table cell. T
178
193
  "fieldInput": {
179
194
  "format": "relation",
180
195
  "resource": "./author/resource.json",
181
- "options": { "display": "autocomplete", "displayKey": "name" }
196
+ "options": {
197
+ "display": "autocomplete",
198
+ "displayKey": "name"
199
+ }
182
200
  }
183
201
  }
184
202
  },
185
203
  "layout": {
186
204
  "view": {
187
205
  "items": [
188
- { "id": "author", "options": { "display": "link" } }
206
+ {
207
+ "id": "author",
208
+ "options": {
209
+ "display": "link"
210
+ }
211
+ }
189
212
  ]
190
213
  },
191
214
  "table": {
192
215
  "items": [
193
- { "id": "author", "options": { "displayKey": "shortName" } }
216
+ {
217
+ "id": "author",
218
+ "options": {
219
+ "displayKey": "shortName"
220
+ }
221
+ }
194
222
  ]
195
223
  }
196
224
  }
@@ -208,7 +236,12 @@ Add `customComponent` to the table layout item's `options` to override the cell
208
236
  "layout": {
209
237
  "table": {
210
238
  "items": [
211
- { "id": "section_number", "options": { "customComponent": "moveUpDown" } }
239
+ {
240
+ "id": "section_number",
241
+ "options": {
242
+ "customComponent": "moveUpDown"
243
+ }
244
+ }
212
245
  ]
213
246
  }
214
247
  }
@@ -236,12 +269,15 @@ Read-only columns derived from a raw SQL subquery at query time. Add a `computed
236
269
  }
237
270
  ```
238
271
 
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"`. |
272
+ | Field | Type | Description |
273
+ |--------|----------|-----------------------------------------------------------------------------|
274
+ | `sql` | `string` | Raw SQL subquery. Use `main` as the alias for the parent table row. |
275
+ | `type` | `string` | Cast type for the result: `"number"` (default), `"string"`, or `"boolean"`. |
243
276
 
244
- All standard column fields (`label`, `hiddenInTable`, `hiddenInView`, `position`, `fieldInput`, …) are valid alongside `computed`.
277
+ All standard column fields (`label`, `hiddenInTable`, `hiddenInView`, `position`, `fieldInput`, …) are valid alongside
278
+ `computed`. When a context declares a layout, list the computed column's id in `layout.table.items` /
279
+ `layout.view.items` to place it there (it is hidden otherwise); with no layout it follows `hiddenInTable` /
280
+ `hiddenInView` as before.
245
281
 
246
282
  ::: warning
247
283
  Computed columns are not available on `kind: "custom"` resources — they run raw SQL against a real table.
@@ -249,12 +285,18 @@ Compute the value in `findAll` instead.
249
285
  :::
250
286
 
251
287
  ::: warning Deprecated: `calculatedColumns`
252
- The top-level `calculatedColumns` array is removed. Run `crouton migrate` to move each entry to `columns[id].computed`.
288
+ The top-level `calculatedColumns` array is removed — computed columns live on
289
+ `columns[id].computed`. Legacy top-level entries no longer accept `hiddenInTable` /
290
+ `hiddenInView`; without an explicit layout such a column is always rendered.
253
291
  :::
254
292
 
255
293
  ## Includes
256
294
 
257
- Eagerly load relations with the list/detail queries. Declare them per operation so `findAll` and `findOne` can differ:
295
+ Eagerly load relations with the list/detail queries. Declare them per operation so `findAll` and `findOne` can differ.
296
+
297
+ ### Dot-path strings (recommended for simple nesting)
298
+
299
+ Use `"a.b"` shorthand instead of the verbose object form when you don't need `orderBy`:
258
300
 
259
301
  ```json
260
302
  {
@@ -262,13 +304,43 @@ Eagerly load relations with the list/detail queries. Declare them per operation
262
304
  "findAll": {
263
305
  "include": [
264
306
  "author",
265
- { "relation": "chapters", "orderBy": { "position": "asc" }, "include": ["sections"] }
307
+ "chapters.sections"
266
308
  ]
267
- },
268
- "findOne": {
309
+ }
310
+ }
311
+ }
312
+ ```
313
+
314
+ Multiple dot-path entries for the same top-level relation are **deep-merged**:
315
+
316
+ ```json
317
+ "include": ["a.b", "a.c"]
318
+ // → { a: { include: { b: true, c: true } } }
319
+ ```
320
+
321
+ Mix dot paths and object form freely — they merge on the same relation:
322
+
323
+ ```json
324
+ "include": [
325
+ "chapters.sections",
326
+ { "relation": "chapters", "orderBy": { "position": "asc" } }
327
+ ]
328
+ // → chapters included with sections nested + orderBy applied
329
+ ```
330
+
331
+ ### Object form (when you need options)
332
+
333
+ ```json
334
+ {
335
+ "operations": {
336
+ "findAll": {
269
337
  "include": [
270
338
  "author",
271
- { "relation": "chapters", "orderBy": { "position": "asc" }, "include": ["sections"] }
339
+ {
340
+ "relation": "chapters",
341
+ "orderBy": { "position": "asc" },
342
+ "include": ["sections"]
343
+ }
272
344
  ]
273
345
  }
274
346
  }
@@ -277,15 +349,19 @@ Eagerly load relations with the list/detail queries. Declare them per operation
277
349
 
278
350
  When `findOne.include` is absent, crouton falls back to `findAll.include`.
279
351
 
280
- | Field | Type | Description |
281
- |------------|------------------|--------------------------------------------------------------------|
282
- | `relation` | `string` | Prisma relation field name |
283
- | `orderBy` | `object` | Optional sort: `{ field: 'asc' \| 'desc' }` for included records |
284
- | `include` | `array` | Nested includes |
352
+ | Field | Type | Description |
353
+ |------------|----------|-------------------------------------------------------------------|
354
+ | `relation` | `string` | Prisma relation field name |
355
+ | `orderBy` | `object` | Optional sort: `{ field: 'asc' \| 'desc' }` for included records |
356
+ | `include` | `array` | Nested includes (string, dot-path, or object) |
285
357
 
286
358
  When a relation column has a `sort` option (see [Relations](relations.md#sorting-related-records)), the loader
287
359
  automatically injects the corresponding `orderBy` into the include clause — no manual configuration needed.
288
360
 
361
+ `?include=` is limited to relation columns the **table layout** renders (via `layout.table.items`); without a
362
+ table layout it falls back to the `hiddenInTable` column flag. A relation the table never shows cannot be included.
363
+
289
364
  ::: 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`.
365
+ The top-level `include` field is removed. Move it to `operations.findAll.include` /
366
+ `operations.findOne.include`.
291
367
  :::
@@ -39,4 +39,30 @@ Autocomplete with optional free-text and create. Three option shapes are support
39
39
  }
40
40
  ```
41
41
 
42
+ ## Excluding already-selected values
43
+
44
+ Use `exclude` to hide values the user already chose — useful for many-to-many chip lists or sibling field constraints.
45
+
46
+ ```jsonc
47
+ "fieldInput": {
48
+ "type": "autocomplete",
49
+ "options": {
50
+ "resource": "./author.resource",
51
+ "labelKey": "name",
52
+ "valueKey": "id",
53
+ "exclude": {
54
+ "values": "{form.coAuthors}", // {form.path} placeholder or a literal array
55
+ "key": "id", // filter field — defaults to valueKey
56
+ "selected": true // also exclude this field's own current value
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ - **`values`** — a `{form.path}` placeholder (resolved at fetch time) or a literal `unknown[]`. Objects in the array are mapped through `valueKey` to extract the scalar.
63
+ - **`key`** — the server-side field used for the `not_in` filter. Defaults to `valueKey` (or `"id"`).
64
+ - **`selected`** — when `true`, the control's own current value is also excluded. Automatically applied by `RelationInline` for many-to-many chips.
65
+
66
+ For resource-backed autocomplete the exclude list is sent as a `filter=<key>:[…]:not_in` query param (skipped above 200 values). For remote URIs, add `{exclude}` to the URI template. Client-side filtering always runs as a fallback.
67
+
42
68
  <!-- @include: ./_generated/autocomplete.md -->