@stapel/forms-react 0.1.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.
Files changed (203) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/MODULE.md +172 -0
  3. package/README.md +207 -0
  4. package/dist/api/export.d.ts +66 -0
  5. package/dist/api/export.d.ts.map +1 -0
  6. package/dist/api/export.js +138 -0
  7. package/dist/api/export.js.map +1 -0
  8. package/dist/api/extensions.d.ts +8 -0
  9. package/dist/api/extensions.d.ts.map +1 -0
  10. package/dist/api/extensions.js +8 -0
  11. package/dist/api/extensions.js.map +1 -0
  12. package/dist/api/formsApi.d.ts +113 -0
  13. package/dist/api/formsApi.d.ts.map +1 -0
  14. package/dist/api/formsApi.js +81 -0
  15. package/dist/api/formsApi.js.map +1 -0
  16. package/dist/api/generated/schema.d.ts +987 -0
  17. package/dist/api/generated/schema.d.ts.map +1 -0
  18. package/dist/api/generated/schema.js +2 -0
  19. package/dist/api/generated/schema.js.map +1 -0
  20. package/dist/api/types.d.ts +222 -0
  21. package/dist/api/types.d.ts.map +1 -0
  22. package/dist/api/types.js +14 -0
  23. package/dist/api/types.js.map +1 -0
  24. package/dist/default/ConfigField.d.ts +32 -0
  25. package/dist/default/ConfigField.d.ts.map +1 -0
  26. package/dist/default/ConfigField.js +101 -0
  27. package/dist/default/ConfigField.js.map +1 -0
  28. package/dist/default/ErrorAlert.d.ts +9 -0
  29. package/dist/default/ErrorAlert.d.ts.map +1 -0
  30. package/dist/default/ErrorAlert.js +27 -0
  31. package/dist/default/ErrorAlert.js.map +1 -0
  32. package/dist/default/FormBuilderPane.d.ts +36 -0
  33. package/dist/default/FormBuilderPane.d.ts.map +1 -0
  34. package/dist/default/FormBuilderPane.js +70 -0
  35. package/dist/default/FormBuilderPane.js.map +1 -0
  36. package/dist/default/FormsListPane.d.ts +18 -0
  37. package/dist/default/FormsListPane.d.ts.map +1 -0
  38. package/dist/default/FormsListPane.js +28 -0
  39. package/dist/default/FormsListPane.js.map +1 -0
  40. package/dist/default/ResponsesPane.d.ts +21 -0
  41. package/dist/default/ResponsesPane.d.ts.map +1 -0
  42. package/dist/default/ResponsesPane.js +118 -0
  43. package/dist/default/ResponsesPane.js.map +1 -0
  44. package/dist/default/StapelForm.d.ts +76 -0
  45. package/dist/default/StapelForm.d.ts.map +1 -0
  46. package/dist/default/StapelForm.js +118 -0
  47. package/dist/default/StapelForm.js.map +1 -0
  48. package/dist/default/fields.d.ts +11 -0
  49. package/dist/default/fields.d.ts.map +1 -0
  50. package/dist/default/fields.js +273 -0
  51. package/dist/default/fields.js.map +1 -0
  52. package/dist/default/index.d.ts +42 -0
  53. package/dist/default/index.d.ts.map +1 -0
  54. package/dist/default/index.js +37 -0
  55. package/dist/default/index.js.map +1 -0
  56. package/dist/default/slots.d.ts +59 -0
  57. package/dist/default/slots.d.ts.map +1 -0
  58. package/dist/default/slots.js +25 -0
  59. package/dist/default/slots.js.map +1 -0
  60. package/dist/default/theme.d.ts +23 -0
  61. package/dist/default/theme.d.ts.map +1 -0
  62. package/dist/default/theme.js +38 -0
  63. package/dist/default/theme.js.map +1 -0
  64. package/dist/default/types.d.ts +12 -0
  65. package/dist/default/types.d.ts.map +1 -0
  66. package/dist/default/types.js +2 -0
  67. package/dist/default/types.js.map +1 -0
  68. package/dist/flows/errors.d.ts +12 -0
  69. package/dist/flows/errors.d.ts.map +1 -0
  70. package/dist/flows/errors.js +13 -0
  71. package/dist/flows/errors.js.map +1 -0
  72. package/dist/flows/registry.d.ts +22 -0
  73. package/dist/flows/registry.d.ts.map +1 -0
  74. package/dist/flows/registry.js +20 -0
  75. package/dist/flows/registry.js.map +1 -0
  76. package/dist/headless/FormBuilder.d.ts +100 -0
  77. package/dist/headless/FormBuilder.d.ts.map +1 -0
  78. package/dist/headless/FormBuilder.js +278 -0
  79. package/dist/headless/FormBuilder.js.map +1 -0
  80. package/dist/headless/FormFill.d.ts +88 -0
  81. package/dist/headless/FormFill.d.ts.map +1 -0
  82. package/dist/headless/FormFill.js +279 -0
  83. package/dist/headless/FormFill.js.map +1 -0
  84. package/dist/headless/FormList.d.ts +34 -0
  85. package/dist/headless/FormList.d.ts.map +1 -0
  86. package/dist/headless/FormList.js +37 -0
  87. package/dist/headless/FormList.js.map +1 -0
  88. package/dist/headless/FormsProvider.d.ts +19 -0
  89. package/dist/headless/FormsProvider.d.ts.map +1 -0
  90. package/dist/headless/FormsProvider.js +15 -0
  91. package/dist/headless/FormsProvider.js.map +1 -0
  92. package/dist/headless/ResponsesTable.d.ts +82 -0
  93. package/dist/headless/ResponsesTable.d.ts.map +1 -0
  94. package/dist/headless/ResponsesTable.js +195 -0
  95. package/dist/headless/ResponsesTable.js.map +1 -0
  96. package/dist/i18n/errorsMap.d.ts +10 -0
  97. package/dist/i18n/errorsMap.d.ts.map +1 -0
  98. package/dist/i18n/errorsMap.js +20 -0
  99. package/dist/i18n/errorsMap.js.map +1 -0
  100. package/dist/i18n/es.d.ts +7 -0
  101. package/dist/i18n/es.d.ts.map +1 -0
  102. package/dist/i18n/es.js +118 -0
  103. package/dist/i18n/es.js.map +1 -0
  104. package/dist/i18n/generated/errors.es.gen.d.ts +16 -0
  105. package/dist/i18n/generated/errors.es.gen.d.ts.map +1 -0
  106. package/dist/i18n/generated/errors.es.gen.js +79 -0
  107. package/dist/i18n/generated/errors.es.gen.js.map +1 -0
  108. package/dist/i18n/generated/errors.gen.d.ts +485 -0
  109. package/dist/i18n/generated/errors.gen.d.ts.map +1 -0
  110. package/dist/i18n/generated/errors.gen.js +246 -0
  111. package/dist/i18n/generated/errors.gen.js.map +1 -0
  112. package/dist/i18n/generated/errors.ru.gen.d.ts +16 -0
  113. package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -0
  114. package/dist/i18n/generated/errors.ru.gen.js +79 -0
  115. package/dist/i18n/generated/errors.ru.gen.js.map +1 -0
  116. package/dist/i18n/keys.d.ts +141 -0
  117. package/dist/i18n/keys.d.ts.map +1 -0
  118. package/dist/i18n/keys.js +233 -0
  119. package/dist/i18n/keys.js.map +1 -0
  120. package/dist/i18n/ru.d.ts +7 -0
  121. package/dist/i18n/ru.d.ts.map +1 -0
  122. package/dist/i18n/ru.js +127 -0
  123. package/dist/i18n/ru.js.map +1 -0
  124. package/dist/index.d.ts +61 -0
  125. package/dist/index.d.ts.map +1 -0
  126. package/dist/index.js +55 -0
  127. package/dist/index.js.map +1 -0
  128. package/dist/model/context.d.ts +11 -0
  129. package/dist/model/context.d.ts.map +1 -0
  130. package/dist/model/context.js +16 -0
  131. package/dist/model/context.js.map +1 -0
  132. package/dist/model/mutations.d.ts +135 -0
  133. package/dist/model/mutations.d.ts.map +1 -0
  134. package/dist/model/mutations.js +223 -0
  135. package/dist/model/mutations.js.map +1 -0
  136. package/dist/model/queries.d.ts +59 -0
  137. package/dist/model/queries.d.ts.map +1 -0
  138. package/dist/model/queries.js +117 -0
  139. package/dist/model/queries.js.map +1 -0
  140. package/dist/model/queryKeys.d.ts +28 -0
  141. package/dist/model/queryKeys.d.ts.map +1 -0
  142. package/dist/model/queryKeys.js +23 -0
  143. package/dist/model/queryKeys.js.map +1 -0
  144. package/dist/model/runtime.d.ts +28 -0
  145. package/dist/model/runtime.d.ts.map +1 -0
  146. package/dist/model/runtime.js +14 -0
  147. package/dist/model/runtime.js.map +1 -0
  148. package/dist/widgets/configForms.d.ts +112 -0
  149. package/dist/widgets/configForms.d.ts.map +1 -0
  150. package/dist/widgets/configForms.js +211 -0
  151. package/dist/widgets/configForms.js.map +1 -0
  152. package/dist/widgets/registry.d.ts +83 -0
  153. package/dist/widgets/registry.d.ts.map +1 -0
  154. package/dist/widgets/registry.js +27 -0
  155. package/dist/widgets/registry.js.map +1 -0
  156. package/dist/widgets/validate.d.ts +57 -0
  157. package/dist/widgets/validate.d.ts.map +1 -0
  158. package/dist/widgets/validate.js +173 -0
  159. package/dist/widgets/validate.js.map +1 -0
  160. package/llms.txt +124 -0
  161. package/manifest.json +1038 -0
  162. package/package.json +108 -0
  163. package/src/analytics/generated/events.json +7 -0
  164. package/src/api/export.ts +195 -0
  165. package/src/api/extensions.ts +7 -0
  166. package/src/api/formsApi.ts +329 -0
  167. package/src/api/generated/schema.ts +989 -0
  168. package/src/api/types.ts +276 -0
  169. package/src/default/ConfigField.tsx +227 -0
  170. package/src/default/ErrorAlert.tsx +46 -0
  171. package/src/default/FormBuilderPane.tsx +414 -0
  172. package/src/default/FormsListPane.tsx +144 -0
  173. package/src/default/ResponsesPane.tsx +332 -0
  174. package/src/default/StapelForm.tsx +377 -0
  175. package/src/default/fields.tsx +454 -0
  176. package/src/default/index.ts +71 -0
  177. package/src/default/slots.ts +78 -0
  178. package/src/default/theme.tsx +65 -0
  179. package/src/default/types.ts +12 -0
  180. package/src/flows/errors.ts +16 -0
  181. package/src/flows/registry.ts +32 -0
  182. package/src/headless/FormBuilder.tsx +453 -0
  183. package/src/headless/FormFill.tsx +396 -0
  184. package/src/headless/FormList.tsx +74 -0
  185. package/src/headless/FormsProvider.tsx +20 -0
  186. package/src/headless/ResponsesTable.tsx +341 -0
  187. package/src/i18n/errorsMap.ts +31 -0
  188. package/src/i18n/es.ts +140 -0
  189. package/src/i18n/generated/errors.es.gen.ts +85 -0
  190. package/src/i18n/generated/errors.gen.ts +277 -0
  191. package/src/i18n/generated/errors.json +583 -0
  192. package/src/i18n/generated/errors.ru.gen.ts +85 -0
  193. package/src/i18n/keys.ts +264 -0
  194. package/src/i18n/ru.ts +150 -0
  195. package/src/index.ts +184 -0
  196. package/src/model/context.tsx +28 -0
  197. package/src/model/mutations.ts +387 -0
  198. package/src/model/queries.ts +154 -0
  199. package/src/model/queryKeys.ts +61 -0
  200. package/src/model/runtime.ts +48 -0
  201. package/src/widgets/registry.ts +101 -0
  202. package/src/widgets/validate.ts +224 -0
  203. package/tsconfig.json +26 -0
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Wire types for the stapel-forms HTTP contract — **derived from the generated
3
+ * OpenAPI surface** (frontend-standard §2/§3), never hand-maintained. The
4
+ * single source of truth is `components["schemas"]` from this pair's own
5
+ * package-LOCAL generated schema (`./generated/schema.js`, produced by
6
+ * `pnpm gen:api` from stapel-forms's OWN `docs/schema.json` — the §17-native
7
+ * per-module contract). Alias the schemas this pair uses under local names
8
+ * here; do NOT write parallel response bodies. Where drf-spectacular +
9
+ * openapi-typescript under-describe the runtime, apply a small documented
10
+ * correction — each one below states WHAT the generator lost and WHY the
11
+ * correction is safe.
12
+ */
13
+ import type { components } from "./generated/schema.js";
14
+
15
+ /** The generated schema table — the one source of truth for wire shapes. */
16
+ export type Schemas = components["schemas"];
17
+
18
+ // ── the field vocabulary ─────────────────────────────────────────────────────
19
+
20
+ /**
21
+ * The ten attribute kinds a stapel-forms schema may use
22
+ * (`STAPEL_FORMS["FIELD_KINDS"]`, `conf.py::DEFAULT_FIELD_KINDS`). A host that
23
+ * registers an extra `stapel_attributes` type server-side can put a kind here
24
+ * that this union does not name — which is exactly why {@link FormFieldDef}
25
+ * types `kind` as a plain `string` and the widget registry answers "unsupported
26
+ * field" instead of crashing. This union is the CLOSED set the pair ships
27
+ * builtin widgets for, not a claim about what the server may send.
28
+ */
29
+ export type BuiltinFieldKind =
30
+ | "string"
31
+ | "int"
32
+ | "float"
33
+ | "bool"
34
+ | "select"
35
+ | "date"
36
+ | "header"
37
+ | "hex_color"
38
+ | "hierarchical_select"
39
+ | "convertible_unit";
40
+
41
+ /** Every {@link BuiltinFieldKind}, in the order the builder offers them. */
42
+ export const BUILTIN_FIELD_KINDS: readonly BuiltinFieldKind[] = [
43
+ "string",
44
+ "int",
45
+ "float",
46
+ "bool",
47
+ "select",
48
+ "date",
49
+ "header",
50
+ "hex_color",
51
+ "hierarchical_select",
52
+ "convertible_unit",
53
+ ];
54
+
55
+ /**
56
+ * One field of a form schema — a `stapel_attributes` `FeatureDef` dict.
57
+ *
58
+ * GENERATOR CORRECTION. `PublicFormDTO.fields` is declared
59
+ * `{[key: string]: unknown}[]`: the backend serializes whatever
60
+ * `coerce_feature_defs` accepts and drf-spectacular cannot see inside it. The
61
+ * shape IS pinned by `stapel_attributes.FeatureDef` (slug/config/name/mandatory
62
+ * /show_at_title/show_as_badge/translate) and by `stapel_forms.schema`'s
63
+ * validator, so naming the fields here is a correction, not an invention. The
64
+ * `& Record<string, unknown>` tail keeps an EXTRA_TYPES field's own keys
65
+ * reachable without a cast.
66
+ *
67
+ * `config` keys are **camelCase** (`maxLength`, not `max_length`) — backend
68
+ * delta note 1: the type dataclasses drop unknown keys silently, so a
69
+ * snake_case typo is a constraint that does not exist. `publish` refuses those
70
+ * with `error.400.forms_invalid_schema` carrying `params.key`.
71
+ */
72
+ export type FormFieldDef = {
73
+ /** Answer key. Unique per schema (`error.400.forms_duplicate_slug`). */
74
+ readonly slug: string;
75
+ /** The attribute type. A kind outside {@link BuiltinFieldKind} is legal
76
+ * server-side and renders through the unsupported-field notice. */
77
+ readonly kind: string;
78
+ /** Admin-authored label. NOT an i18n key — see the i18n note in README. */
79
+ readonly name?: string;
80
+ /** Kind-specific configuration, camelCase keys. */
81
+ readonly config?: Readonly<Record<string, unknown>>;
82
+ /** Required — the client mirrors it, the server decides it. */
83
+ readonly mandatory?: boolean;
84
+ readonly show_at_title?: boolean;
85
+ readonly show_as_badge?: boolean;
86
+ readonly translate?: boolean;
87
+ } & Record<string, unknown>;
88
+
89
+ /**
90
+ * A form schema's non-field metadata (`stapel_forms.schema.META_KEYS`). Every
91
+ * key is optional and admin-authored; `logic` is deliberately absent (v1 has no
92
+ * conditional branching, and reserving the key keeps adding it additive).
93
+ */
94
+ export interface FormSchemaMeta {
95
+ readonly title?: string;
96
+ readonly description?: string;
97
+ readonly confirmation_text?: string;
98
+ readonly submit_label?: string;
99
+ }
100
+
101
+ /** The stored/authored schema envelope: `{fields, meta}`. */
102
+ export interface FormSchema {
103
+ readonly fields: readonly FormFieldDef[];
104
+ readonly meta?: FormSchemaMeta;
105
+ }
106
+
107
+ // ── the public (anonymous) surface ───────────────────────────────────────────
108
+
109
+ /**
110
+ * `GET /public/<public_id>/` 200 — what an anonymous respondent may know.
111
+ *
112
+ * GENERATOR CORRECTION, twice over: `fields` is narrowed to
113
+ * {@link FormFieldDef}[] (see above) and made REQUIRED. drf-spectacular marks
114
+ * only `public_id`/`version`/`version_id` required because the presenter's
115
+ * dataclass gives `fields`/`meta` defaults — but the view never omits them, and
116
+ * a renderer that must branch on `fields === undefined` would be branching on a
117
+ * case the server cannot produce.
118
+ */
119
+ export type PublicForm = Omit<Schemas["PublicFormDTO"], "fields" | "meta"> & {
120
+ readonly fields: readonly FormFieldDef[];
121
+ readonly meta: FormSchemaMeta;
122
+ };
123
+
124
+ /** `POST /public/<public_id>/submissions/` body. `answers` are BARE SCALARS
125
+ * keyed by slug — a `select` answer may be a single value and the server
126
+ * normalizes it to a list. `version_id` is echoed back from the rendered
127
+ * schema; omitting it forfeits the clean 409 and risks mis-validation. */
128
+ export interface SubmitRequest {
129
+ readonly answers: Readonly<Record<string, unknown>>;
130
+ readonly version_id?: string | null;
131
+ /** The captcha seam (spec §12 risk 3): the netintel tier decides whether a
132
+ * token is required at all, so it stays optional and the captcha layer
133
+ * refuses on its own terms. */
134
+ readonly captcha_token?: string;
135
+ }
136
+
137
+ /** `POST .../submissions/` 201 — deliberately NOT the submission id (there is
138
+ * no public read of submissions). */
139
+ export type SubmitResult = Schemas["SubmitResultDTO"];
140
+
141
+ // ── the admin surface ────────────────────────────────────────────────────────
142
+
143
+ /** `open | closed | draft`. */
144
+ export type FormState = Schemas["StateEnum"];
145
+
146
+ /**
147
+ * A `Form` row as its workspace's admins see it.
148
+ *
149
+ * GENERATOR CORRECTION: `draft_schema` is narrowed from `{[k: string]: unknown}`
150
+ * to {@link FormSchema} — the builder reads `.fields` off it, and the backend
151
+ * stores exactly the normalized envelope. `settings` keeps its open shape (a
152
+ * host may add keys) but names the destinations the pair drives.
153
+ */
154
+ export type FormRow = Omit<
155
+ Schemas["FormPresenterDTO"],
156
+ "draft_schema" | "settings"
157
+ > & {
158
+ readonly draft_schema: FormSchema | null;
159
+ readonly settings: FormSettings;
160
+ };
161
+
162
+ /**
163
+ * `Form.settings` — an open bag whose notification keys the pair drives.
164
+ * Backend delta note 4: the spec's `notify_channels` was NOT built. Channels
165
+ * are a property of the routing entry; what a form needs is DESTINATIONS, so
166
+ * these keys name destinations and map to `request_notification` keywords
167
+ * through `notifications.TARGET_KINDS`.
168
+ */
169
+ export type FormSettings = {
170
+ readonly notify_emails?: readonly string[];
171
+ readonly notify_telegram_chat_ids?: readonly string[];
172
+ } & Record<string, unknown>;
173
+
174
+ /** `POST /forms` body. */
175
+ export interface FormCreateRequest {
176
+ readonly workspace_id: string;
177
+ readonly title: string;
178
+ readonly settings?: FormSettings;
179
+ readonly draft_schema?: FormSchema;
180
+ }
181
+
182
+ /** `PATCH /forms/<id>` body — title and settings only; the schema moves
183
+ * through `draft` + `publish`, never through a partial update. */
184
+ export interface FormPatchRequest {
185
+ readonly title?: string;
186
+ readonly settings?: FormSettings;
187
+ }
188
+
189
+ /** `POST /forms/<id>/publish` 200 — the new immutable version. */
190
+ export type PublishResult = Schemas["PublishResultDTO"];
191
+
192
+ /** One published, immutable schema version. */
193
+ export type FormVersion = Omit<Schemas["FormVersionPresenterDTO"], "schema"> & {
194
+ readonly schema: FormSchema;
195
+ };
196
+
197
+ /** One response, as a reviewer sees it. `answers` is `{slug: value}` (the raw
198
+ * DAO's display metadata is dropped — the reviewer has it from the version). */
199
+ export type Submission = Schemas["SubmissionPresenterDTO"];
200
+
201
+ /** `POST /submissions/<id>/resend` body. Backend delta note 7: given either
202
+ * list, the form's configured targets are REPLACED, not supplemented — "send
203
+ * this one to legal" must not also re-send it to everyone who already has it. */
204
+ export interface ResendRequest {
205
+ readonly recipients?: readonly string[];
206
+ readonly telegram_chat_ids?: readonly string[];
207
+ }
208
+
209
+ /** `POST /submissions/<id>/resend` 200. */
210
+ export type ResendResult = Schemas["ResendResultDTO"];
211
+
212
+ /** Query for the keyset-paginated submission list. */
213
+ export interface SubmissionListParams {
214
+ readonly workspaceId: string;
215
+ readonly formId: string;
216
+ /** Keyset cursor — the previous page's last `submitted_at`. */
217
+ readonly before?: string;
218
+ readonly limit?: number;
219
+ /** Restrict to answers of one schema version. */
220
+ readonly version?: number;
221
+ }
222
+
223
+ // ── the field-kind catalogue (the builder's dictionary) ──────────────────────
224
+
225
+ /**
226
+ * One declared config-form field, passed through from stapel-attributes'
227
+ * `config_form.FormField.to_dict()` VERBATIM by the backend — so a kind gains
228
+ * config fields upstream with no release on either side of the wire.
229
+ *
230
+ * GENERATOR CORRECTION: `FieldKindDTO.fields` is declared
231
+ * `{[key: string]: unknown}[]` because drf-spectacular cannot see inside a
232
+ * pass-through. The shape is pinned by `FormField.to_dict()`, so naming it here
233
+ * is a correction rather than an invention; the `& Record<string, unknown>`
234
+ * tail keeps a host widget's own params reachable without a cast.
235
+ */
236
+ export type ConfigFieldSpec = {
237
+ /** The config key this field edits. Order within a kind is significant. */
238
+ readonly name: string;
239
+ /** A key of {@link FieldKindCatalogue.configWidgets} — the WIDGET
240
+ * vocabulary, not the feature-type vocabulary. */
241
+ readonly kind: string;
242
+ /** Upstream i18n key, `admin.attributes.form.<type>.<field>`. */
243
+ readonly label_key: string;
244
+ /** Cosmetic `*` marker only — real validation is server-side. */
245
+ readonly required?: boolean;
246
+ /** Applied when the config key is absent. */
247
+ readonly default?: unknown;
248
+ /** Widget-specific params (`step`, `itemType`, inline `options`,
249
+ * `placeholder`, …). Only keys the widget understands are meaningful. */
250
+ readonly params?: Readonly<Record<string, unknown>>;
251
+ } & Record<string, unknown>;
252
+
253
+ /** One field kind the builder may offer. */
254
+ export type FieldKind = Omit<Schemas["FieldKindDTO"], "fields"> & {
255
+ /**
256
+ * The kind's config-form declaration. **Empty means the kind declares no
257
+ * config form** (upstream `BUILTIN_FORMS` simply has no entry — this is how
258
+ * `convertible_unit` arrives), which is one of the two builder-less signals.
259
+ */
260
+ readonly fields: readonly ConfigFieldSpec[];
261
+ };
262
+
263
+ /**
264
+ * `GET /field-kinds` 200 — the whole catalogue behind the builder.
265
+ *
266
+ * `config_widgets` is upstream's `config_form.FIELD_KINDS`: the widget
267
+ * vocabulary a declaration's `kind` draws from, mapped to the params each
268
+ * widget understands. Named apart from `kinds` on purpose — forms' own
269
+ * `FIELD_KINDS` setting is the feature-TYPE allowlist, and two different things
270
+ * under one name is how a builder ends up drawing a `number` where a `string`
271
+ * belongs.
272
+ */
273
+ export interface FieldKindCatalogue {
274
+ readonly kinds: readonly FieldKind[];
275
+ readonly configWidgets: Readonly<Record<string, readonly string[]>>;
276
+ }
@@ -0,0 +1,227 @@
1
+ /**
2
+ * One row of a field's config form, chosen by the CONFIG-WIDGET KIND — the
3
+ * mechanism that makes `<FormBuilderPane>` data-driven rather than a
4
+ * hand-written form per attribute type.
5
+ *
6
+ * `stapel_attributes.config_form` declares each feature type's admin form as
7
+ * a list of `FormField(name, kind, …)` where `kind` comes from a fixed
8
+ * 13-entry vocabulary (`FIELD_KINDS`), and stapel-forms 0.2.0 serves those
9
+ * declarations at `GET /field-kinds`. This file implements the WIDGET
10
+ * vocabulary once, so a feature type registered by a host gets a working
11
+ * config form with no new UI — and, now that the declarations are fetched
12
+ * rather than mirrored, with no client release either.
13
+ *
14
+ * Two of upstream's 13 widgets are not implemented here — `hierarchical_options`
15
+ * (a tree editor) and `timestamp_array`. Their rows render with an explanation
16
+ * rather than vanishing: a config form that hides an option looks complete when
17
+ * it is not. The `default:` arm catches those two AND anything a future
18
+ * attributes release adds, so a new widget degrades loudly instead of writing a
19
+ * wrong shape into a published schema.
20
+ */
21
+ import type { ReactElement } from "react";
22
+ import { Input, InputNumber, Select, Switch, Typography } from "antd";
23
+ import { useT } from "@stapel/core";
24
+ import type { ConfigFieldSpec } from "../api/types.js";
25
+ import { FORMS_I18N_KEYS } from "../i18n/keys.js";
26
+
27
+ function str(value: unknown): string {
28
+ return typeof value === "string" ? value : value == null ? "" : String(value);
29
+ }
30
+
31
+ function numberish(value: unknown): number | undefined {
32
+ if (typeof value === "number" && Number.isFinite(value)) return value;
33
+ return undefined;
34
+ }
35
+
36
+ /**
37
+ * Widget params live under `spec.params`, not flattened onto the spec — that
38
+ * is `FormField.to_dict()`'s real shape, which the pair only saw once the
39
+ * declarations came off the wire instead of a hand-written mirror.
40
+ */
41
+ function param(spec: ConfigFieldSpec, key: string): unknown {
42
+ return (spec.params ?? {})[key];
43
+ }
44
+
45
+ function paramNumber(spec: ConfigFieldSpec, key: string): number | undefined {
46
+ const raw = param(spec, key);
47
+ return typeof raw === "number" && Number.isFinite(raw) ? raw : undefined;
48
+ }
49
+
50
+ function paramString(spec: ConfigFieldSpec, key: string): string | undefined {
51
+ const raw = param(spec, key);
52
+ return typeof raw === "string" && raw.length > 0 ? raw : undefined;
53
+ }
54
+
55
+ /** Inline `{value, label}` choices for a `select` config widget. */
56
+ function paramOptions(
57
+ spec: ConfigFieldSpec
58
+ ): readonly { value: string; label: string }[] {
59
+ const raw = param(spec, "options");
60
+ if (!Array.isArray(raw)) return [];
61
+ return raw.flatMap((option) => {
62
+ if (option === null || typeof option !== "object") return [];
63
+ const entry = option as { value?: unknown; label?: unknown };
64
+ const value = str(entry.value);
65
+ return [{ value, label: entry.label == null ? value : str(entry.label) }];
66
+ });
67
+ }
68
+
69
+ /** A tag-style list editor for the `*_options` kinds. `number_options`
70
+ * coerces back to numbers so a numeric option list does not silently become
71
+ * a list of strings the engine will refuse. */
72
+ function OptionsList(props: {
73
+ value: unknown;
74
+ numeric: boolean;
75
+ disabled: boolean;
76
+ onChange(next: unknown): void;
77
+ }): ReactElement {
78
+ const current = Array.isArray(props.value) ? props.value.map(str) : [];
79
+ return (
80
+ <Select
81
+ mode="tags"
82
+ style={{ width: "100%" }}
83
+ value={current}
84
+ disabled={props.disabled}
85
+ open={false}
86
+ suffixIcon={null}
87
+ onChange={(next: string[]) => {
88
+ if (next.length === 0) {
89
+ props.onChange(undefined);
90
+ return;
91
+ }
92
+ props.onChange(
93
+ props.numeric
94
+ ? next.map(Number).filter((n) => Number.isFinite(n))
95
+ : next
96
+ );
97
+ }}
98
+ />
99
+ );
100
+ }
101
+
102
+ export interface ConfigFieldProps {
103
+ readonly spec: ConfigFieldSpec;
104
+ readonly value: unknown;
105
+ readonly disabled: boolean;
106
+ /** `undefined` REMOVES the key — an absent config key means "the engine's
107
+ * own default", which is not the same as a stored `null`. */
108
+ onChange(value: unknown): void;
109
+ }
110
+
111
+ export function ConfigField(props: ConfigFieldProps): ReactElement {
112
+ const t = useT();
113
+ const { spec, value, disabled } = props;
114
+
115
+ switch (spec.kind) {
116
+ case "number":
117
+ return (
118
+ <InputNumber
119
+ style={{ width: "100%" }}
120
+ value={numberish(value) ?? null}
121
+ disabled={disabled}
122
+ {...(paramNumber(spec, "step") !== undefined
123
+ ? { step: paramNumber(spec, "step") as number }
124
+ : {})}
125
+ onChange={(next) => props.onChange(next ?? undefined)}
126
+ />
127
+ );
128
+
129
+ case "max_selected_dropdown":
130
+ // Absent = unlimited, which is `SelectConfig.maxSelected = None`. The
131
+ // placeholder says so, and clearing the number restores it.
132
+ return (
133
+ <InputNumber
134
+ style={{ width: "100%" }}
135
+ value={numberish(value) ?? null}
136
+ disabled={disabled}
137
+ min={1}
138
+ step={1}
139
+ placeholder={t(FORMS_I18N_KEYS.fillUnlimited)}
140
+ onChange={(next) => props.onChange(next ?? undefined)}
141
+ />
142
+ );
143
+
144
+ case "text":
145
+ case "translatable_text":
146
+ // v1 edits the base string only. A per-locale editor is the same v2
147
+ // fork as translated form CONTENT (spec §10) — offering one language
148
+ // box per locale here would imply a translation pipeline that does not
149
+ // exist yet.
150
+ return (
151
+ <Input
152
+ value={str(value)}
153
+ disabled={disabled}
154
+ {...(paramString(spec, "placeholder") !== undefined
155
+ ? { placeholder: paramString(spec, "placeholder") as string }
156
+ : {})}
157
+ onChange={(event) => props.onChange(event.target.value || undefined)}
158
+ />
159
+ );
160
+
161
+ case "checkbox":
162
+ return (
163
+ <Switch
164
+ checked={value === true}
165
+ disabled={disabled}
166
+ onChange={(checked) => props.onChange(checked)}
167
+ />
168
+ );
169
+
170
+ case "select":
171
+ return (
172
+ <Select
173
+ style={{ width: "100%" }}
174
+ value={str(value) || null}
175
+ disabled={disabled}
176
+ options={[...paramOptions(spec)]}
177
+ onChange={(next: string) => props.onChange(next)}
178
+ />
179
+ );
180
+
181
+ case "number_options":
182
+ return (
183
+ <OptionsList
184
+ value={value}
185
+ numeric
186
+ disabled={disabled}
187
+ onChange={props.onChange}
188
+ />
189
+ );
190
+
191
+ // `select_options_with_default` shares this editor: the per-option
192
+ // "default" marker is not editable in v1; the option LIST — the part a
193
+ // form actually needs — is.
194
+ case "string_options":
195
+ case "color_options":
196
+ case "select_options_with_default":
197
+ return (
198
+ <OptionsList
199
+ value={value}
200
+ numeric={false}
201
+ disabled={disabled}
202
+ onChange={props.onChange}
203
+ />
204
+ );
205
+
206
+ case "timestamp":
207
+ return (
208
+ <Input
209
+ type="date"
210
+ value={str(value)}
211
+ disabled={disabled}
212
+ onChange={(event) => props.onChange(event.target.value || undefined)}
213
+ />
214
+ );
215
+
216
+ default:
217
+ // `hierarchical_options` (a tree editor) and `timestamp_array` — the two
218
+ // of upstream's 13 config widgets this skin does not implement — plus any
219
+ // widget a future attributes release adds. Say so rather than render a
220
+ // control that would write a wrong shape into a published schema.
221
+ return (
222
+ <Typography.Text type="secondary">
223
+ {t(FORMS_I18N_KEYS.builderUnsupportedConfig, { keys: spec.name })}
224
+ </Typography.Text>
225
+ );
226
+ }
227
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The one error surface this pair's default skin renders, so the split copy
3
+ * core produces reaches a screen the same way everywhere (mirrors the
4
+ * docs/notifications/workspaces etalon).
5
+ *
6
+ * Core's `describeFlowError` splits a failure into the sentence a person
7
+ * reads and the technical detail a support agent quotes (owner report
8
+ * 2026-08-09: the status used to be spliced into the sentence, ending in a
9
+ * bare `" (500)"`). The split only helps if a skin renders both halves at
10
+ * their own weights: `message` at normal weight, `detail` muted and small.
11
+ *
12
+ * `undefined` in — including a `detail` core left `undefined` because there
13
+ * was nothing worth quoting — renders nothing rather than an empty line.
14
+ */
15
+ import { Alert, Typography } from "antd";
16
+ import type { CSSProperties, ReactElement, ReactNode } from "react";
17
+ import type { FlowErrorDisplay } from "@stapel/core";
18
+
19
+ export function ErrorAlert(props: {
20
+ error: FlowErrorDisplay | undefined;
21
+ style?: CSSProperties | undefined;
22
+ testId?: string | undefined;
23
+ action?: ReactNode;
24
+ }): ReactElement | null {
25
+ const { error } = props;
26
+ if (!error) return null;
27
+ return (
28
+ <Alert
29
+ type="error"
30
+ showIcon
31
+ {...(props.style ? { style: props.style } : {})}
32
+ {...(props.testId ? { "data-testid": props.testId } : {})}
33
+ {...(props.action ? { action: props.action } : {})}
34
+ message={error.message}
35
+ {...(error.detail
36
+ ? {
37
+ description: (
38
+ <Typography.Text type="secondary" style={{ fontSize: 12 }}>
39
+ {error.detail}
40
+ </Typography.Text>
41
+ ),
42
+ }
43
+ : {})}
44
+ />
45
+ );
46
+ }