@duffcloudservices/site-forms 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.
- package/README.md +213 -0
- package/dist/DcsForm.vue.d.ts +67 -0
- package/dist/composables/useDcsForm.d.ts +36 -0
- package/dist/composables/useFormSubmission.d.ts +18 -0
- package/dist/composables/useFormValidation.d.ts +19 -0
- package/dist/fields/DcsFormCheckbox.vue.d.ts +12 -0
- package/dist/fields/DcsFormCheckboxGroup.vue.d.ts +12 -0
- package/dist/fields/DcsFormDate.vue.d.ts +14 -0
- package/dist/fields/DcsFormFieldWrapper.vue.d.ts +27 -0
- package/dist/fields/DcsFormFile.vue.d.ts +12 -0
- package/dist/fields/DcsFormHidden.vue.d.ts +7 -0
- package/dist/fields/DcsFormHtmlBlock.vue.d.ts +6 -0
- package/dist/fields/DcsFormRadio.vue.d.ts +12 -0
- package/dist/fields/DcsFormSection.vue.d.ts +21 -0
- package/dist/fields/DcsFormSelect.vue.d.ts +35 -0
- package/dist/fields/DcsFormText.vue.d.ts +34 -0
- package/dist/fields/DcsFormTextarea.vue.d.ts +34 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +918 -0
- package/dist/index.js.map +1 -0
- package/dist/loaders/yaml.d.ts +12 -0
- package/dist/schema/validate.d.ts +9 -0
- package/dist/types.d.ts +106 -0
- package/package.json +73 -0
- package/src/DcsForm.vue +299 -0
- package/src/__tests__/fields.test.ts +82 -0
- package/src/__tests__/multi-step.test.ts +46 -0
- package/src/__tests__/schema.test.ts +42 -0
- package/src/__tests__/submission.test.ts +77 -0
- package/src/__tests__/visible-if.test.ts +111 -0
- package/src/composables/useDcsForm.ts +201 -0
- package/src/composables/useFormSubmission.ts +113 -0
- package/src/composables/useFormValidation.ts +127 -0
- package/src/fields/DcsFormCheckbox.vue +35 -0
- package/src/fields/DcsFormCheckboxGroup.vue +52 -0
- package/src/fields/DcsFormDate.vue +34 -0
- package/src/fields/DcsFormFieldWrapper.vue +39 -0
- package/src/fields/DcsFormFile.vue +38 -0
- package/src/fields/DcsFormHidden.vue +17 -0
- package/src/fields/DcsFormHtmlBlock.vue +19 -0
- package/src/fields/DcsFormRadio.vue +45 -0
- package/src/fields/DcsFormSection.vue +19 -0
- package/src/fields/DcsFormSelect.vue +62 -0
- package/src/fields/DcsFormText.vue +54 -0
- package/src/fields/DcsFormTextarea.vue +43 -0
- package/src/index.ts +51 -0
- package/src/loaders/yaml.ts +51 -0
- package/src/schema/form-definition.schema.json +633 -0
- package/src/schema/validate.ts +58 -0
- package/src/shims.d.ts +10 -0
- package/src/types.ts +140 -0
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://duffcloudservices.com/schemas/form-definition.schema.json",
|
|
4
|
+
"title": "PortalFormDefinition",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"description": "Portable form definition. This schema is the source of truth for\nboth the portal CRUD APIs and the `.dcs/forms/<formId>.yaml`\nfiles consumed by customer-site builds via `<DcsForm/>`.\n",
|
|
7
|
+
"required": [
|
|
8
|
+
"formId",
|
|
9
|
+
"title",
|
|
10
|
+
"submission",
|
|
11
|
+
"fields"
|
|
12
|
+
],
|
|
13
|
+
"properties": {
|
|
14
|
+
"formId": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"description": "Kebab-case identifier, unique per site.",
|
|
17
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$",
|
|
18
|
+
"minLength": 1,
|
|
19
|
+
"maxLength": 80,
|
|
20
|
+
"example": "contact"
|
|
21
|
+
},
|
|
22
|
+
"title": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"description": "Human-readable form title shown in the portal and rendered as the form heading.",
|
|
25
|
+
"minLength": 1,
|
|
26
|
+
"maxLength": 200,
|
|
27
|
+
"example": "Contact Us"
|
|
28
|
+
},
|
|
29
|
+
"description": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"description": "Optional descriptive text rendered above the fields.",
|
|
32
|
+
"maxLength": 2000
|
|
33
|
+
},
|
|
34
|
+
"submitLabel": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"description": "Label for the submit button. Defaults to \"Send\" client-side.",
|
|
37
|
+
"maxLength": 80,
|
|
38
|
+
"example": "Send message"
|
|
39
|
+
},
|
|
40
|
+
"successMessage": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"description": "Confirmation message shown after a successful submission.",
|
|
43
|
+
"maxLength": 2000,
|
|
44
|
+
"example": "Thanks — we'll be in touch shortly."
|
|
45
|
+
},
|
|
46
|
+
"submission": {
|
|
47
|
+
"$ref": "#/definitions/PortalFormSubmissionConfig"
|
|
48
|
+
},
|
|
49
|
+
"steps": {
|
|
50
|
+
"type": "array",
|
|
51
|
+
"description": "Optional multi-step grouping. When omitted, fields render as a single page.",
|
|
52
|
+
"items": {
|
|
53
|
+
"$ref": "#/definitions/PortalFormStep"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"fields": {
|
|
57
|
+
"type": "array",
|
|
58
|
+
"description": "Flat list of all fields in the form. When `steps` is present,\nevery field referenced from a step's `fieldIds` must exist here.\n",
|
|
59
|
+
"items": {
|
|
60
|
+
"$ref": "#/definitions/PortalFormField"
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"version": {
|
|
64
|
+
"type": "integer",
|
|
65
|
+
"minimum": 1,
|
|
66
|
+
"description": "Monotonically increasing version, bumped on each save."
|
|
67
|
+
},
|
|
68
|
+
"createdAt": {
|
|
69
|
+
"type": "string",
|
|
70
|
+
"format": "date-time",
|
|
71
|
+
"description": "Form creation timestamp."
|
|
72
|
+
},
|
|
73
|
+
"updatedAt": {
|
|
74
|
+
"type": "string",
|
|
75
|
+
"format": "date-time",
|
|
76
|
+
"description": "Last modification timestamp."
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"definitions": {
|
|
80
|
+
"PortalFormFieldType": {
|
|
81
|
+
"type": "string",
|
|
82
|
+
"description": "Field type controlling input rendering and validation. Includes\nlayout-only types (`section-heading`, `html-block`) that render\ndecorative content rather than capturing values, and `hidden`\nfor prefilled values not displayed to the user.\n",
|
|
83
|
+
"enum": [
|
|
84
|
+
"text",
|
|
85
|
+
"email",
|
|
86
|
+
"tel",
|
|
87
|
+
"textarea",
|
|
88
|
+
"select",
|
|
89
|
+
"multiselect",
|
|
90
|
+
"radio",
|
|
91
|
+
"checkbox",
|
|
92
|
+
"checkbox-group",
|
|
93
|
+
"date",
|
|
94
|
+
"file",
|
|
95
|
+
"hidden",
|
|
96
|
+
"section-heading",
|
|
97
|
+
"html-block"
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
"PortalFormFieldWidth": {
|
|
101
|
+
"type": "string",
|
|
102
|
+
"description": "Layout hint for the field within its row.",
|
|
103
|
+
"enum": [
|
|
104
|
+
"full",
|
|
105
|
+
"half",
|
|
106
|
+
"third"
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
"PortalFormFieldOption": {
|
|
110
|
+
"type": "object",
|
|
111
|
+
"description": "A single option for select / radio / checkbox-group fields.",
|
|
112
|
+
"required": [
|
|
113
|
+
"value",
|
|
114
|
+
"label"
|
|
115
|
+
],
|
|
116
|
+
"properties": {
|
|
117
|
+
"value": {
|
|
118
|
+
"type": "string",
|
|
119
|
+
"description": "Submitted value for this option."
|
|
120
|
+
},
|
|
121
|
+
"label": {
|
|
122
|
+
"type": "string",
|
|
123
|
+
"description": "Human-readable label shown to the user."
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
"PortalFormFieldValidation": {
|
|
128
|
+
"type": "object",
|
|
129
|
+
"description": "Optional validation rules applied to a field's value.",
|
|
130
|
+
"properties": {
|
|
131
|
+
"regex": {
|
|
132
|
+
"type": "string",
|
|
133
|
+
"description": "ECMAScript-compatible pattern the value must match."
|
|
134
|
+
},
|
|
135
|
+
"minLength": {
|
|
136
|
+
"type": "integer",
|
|
137
|
+
"minimum": 0,
|
|
138
|
+
"description": "Minimum string length (text-like fields only)."
|
|
139
|
+
},
|
|
140
|
+
"maxLength": {
|
|
141
|
+
"type": "integer",
|
|
142
|
+
"minimum": 0,
|
|
143
|
+
"description": "Maximum string length (text-like fields only)."
|
|
144
|
+
},
|
|
145
|
+
"min": {
|
|
146
|
+
"type": "number",
|
|
147
|
+
"description": "Minimum numeric / date value."
|
|
148
|
+
},
|
|
149
|
+
"max": {
|
|
150
|
+
"type": "number",
|
|
151
|
+
"description": "Maximum numeric / date value."
|
|
152
|
+
},
|
|
153
|
+
"accept": {
|
|
154
|
+
"type": "array",
|
|
155
|
+
"description": "Accepted MIME types or file extensions for `file` fields.",
|
|
156
|
+
"items": {
|
|
157
|
+
"type": "string"
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
"PortalFormFieldVisibleIf": {
|
|
163
|
+
"type": "object",
|
|
164
|
+
"description": "Single-predicate visibility rule. The field is shown only when\nthe referenced sibling field's value equals `equals`. Future\nrevisions may extend this with AND / OR composition; clients\nshould treat unknown extra properties as opaque.\n",
|
|
165
|
+
"required": [
|
|
166
|
+
"fieldId",
|
|
167
|
+
"equals"
|
|
168
|
+
],
|
|
169
|
+
"properties": {
|
|
170
|
+
"fieldId": {
|
|
171
|
+
"type": "string",
|
|
172
|
+
"description": "ID of the sibling field whose value gates visibility."
|
|
173
|
+
},
|
|
174
|
+
"equals": {
|
|
175
|
+
"description": "Value (string, number, or boolean) the sibling field must equal.",
|
|
176
|
+
"oneOf": [
|
|
177
|
+
{
|
|
178
|
+
"type": "string"
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"type": "number"
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"type": "boolean"
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
"PortalFormField": {
|
|
191
|
+
"type": "object",
|
|
192
|
+
"description": "A single field within a form definition.",
|
|
193
|
+
"required": [
|
|
194
|
+
"id",
|
|
195
|
+
"type",
|
|
196
|
+
"label"
|
|
197
|
+
],
|
|
198
|
+
"properties": {
|
|
199
|
+
"id": {
|
|
200
|
+
"type": "string",
|
|
201
|
+
"description": "Kebab-case identifier, unique within the form.",
|
|
202
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$",
|
|
203
|
+
"minLength": 1,
|
|
204
|
+
"maxLength": 80
|
|
205
|
+
},
|
|
206
|
+
"type": {
|
|
207
|
+
"$ref": "#/definitions/PortalFormFieldType"
|
|
208
|
+
},
|
|
209
|
+
"label": {
|
|
210
|
+
"type": "string",
|
|
211
|
+
"description": "Human-readable label rendered above the input.",
|
|
212
|
+
"minLength": 1,
|
|
213
|
+
"maxLength": 200
|
|
214
|
+
},
|
|
215
|
+
"helpText": {
|
|
216
|
+
"type": "string",
|
|
217
|
+
"description": "Optional helper / description text shown beneath the label.",
|
|
218
|
+
"maxLength": 500
|
|
219
|
+
},
|
|
220
|
+
"placeholder": {
|
|
221
|
+
"type": "string",
|
|
222
|
+
"description": "Optional placeholder shown inside empty inputs.",
|
|
223
|
+
"maxLength": 200
|
|
224
|
+
},
|
|
225
|
+
"defaultValue": {
|
|
226
|
+
"description": "Optional default value (string, number, boolean, or array of strings).",
|
|
227
|
+
"oneOf": [
|
|
228
|
+
{
|
|
229
|
+
"type": "string"
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"type": "number"
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"type": "boolean"
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"type": "array",
|
|
239
|
+
"items": {
|
|
240
|
+
"type": "string"
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
]
|
|
244
|
+
},
|
|
245
|
+
"required": {
|
|
246
|
+
"type": "boolean",
|
|
247
|
+
"default": false,
|
|
248
|
+
"description": "Whether the field must be supplied to submit the form."
|
|
249
|
+
},
|
|
250
|
+
"width": {
|
|
251
|
+
"allOf": [
|
|
252
|
+
{
|
|
253
|
+
"$ref": "#/definitions/PortalFormFieldWidth"
|
|
254
|
+
}
|
|
255
|
+
],
|
|
256
|
+
"default": "full"
|
|
257
|
+
},
|
|
258
|
+
"options": {
|
|
259
|
+
"type": "array",
|
|
260
|
+
"description": "Options for `select`, `multiselect`, `radio`, `checkbox-group` fields.",
|
|
261
|
+
"items": {
|
|
262
|
+
"$ref": "#/definitions/PortalFormFieldOption"
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
"validation": {
|
|
266
|
+
"$ref": "#/definitions/PortalFormFieldValidation"
|
|
267
|
+
},
|
|
268
|
+
"visibleIf": {
|
|
269
|
+
"$ref": "#/definitions/PortalFormFieldVisibleIf"
|
|
270
|
+
},
|
|
271
|
+
"phi": {
|
|
272
|
+
"type": "boolean",
|
|
273
|
+
"default": false,
|
|
274
|
+
"description": "Marks the field as collecting Protected Health Information.\nWhen true the value must never appear in notification emails\nor logs and the owning site must be in the Medical category\n(see `compliance.instructions.md`).\n"
|
|
275
|
+
},
|
|
276
|
+
"html": {
|
|
277
|
+
"type": "string",
|
|
278
|
+
"description": "Sanitized HTML body for `html-block` fields. Ignored for\nother field types.\n",
|
|
279
|
+
"maxLength": 10000
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
"PortalFormStep": {
|
|
284
|
+
"type": "object",
|
|
285
|
+
"description": "Optional grouping for multi-step (wizard-style) forms such as\nthe KT Braun estate-planning questionnaires. When `steps` is\nomitted on a form, all fields render as a single page.\n",
|
|
286
|
+
"required": [
|
|
287
|
+
"id",
|
|
288
|
+
"title",
|
|
289
|
+
"fieldIds"
|
|
290
|
+
],
|
|
291
|
+
"properties": {
|
|
292
|
+
"id": {
|
|
293
|
+
"type": "string",
|
|
294
|
+
"description": "Kebab-case step identifier, unique within the form.",
|
|
295
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$",
|
|
296
|
+
"minLength": 1,
|
|
297
|
+
"maxLength": 80
|
|
298
|
+
},
|
|
299
|
+
"title": {
|
|
300
|
+
"type": "string",
|
|
301
|
+
"description": "Step title shown in the wizard header.",
|
|
302
|
+
"minLength": 1,
|
|
303
|
+
"maxLength": 200
|
|
304
|
+
},
|
|
305
|
+
"description": {
|
|
306
|
+
"type": "string",
|
|
307
|
+
"description": "Optional descriptive text shown below the step title.",
|
|
308
|
+
"maxLength": 1000
|
|
309
|
+
},
|
|
310
|
+
"fieldIds": {
|
|
311
|
+
"type": "array",
|
|
312
|
+
"description": "Ordered list of field IDs that belong to this step.",
|
|
313
|
+
"items": {
|
|
314
|
+
"type": "string",
|
|
315
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
"PortalFormSubmissionLeadConfig": {
|
|
321
|
+
"type": "object",
|
|
322
|
+
"required": [
|
|
323
|
+
"kind"
|
|
324
|
+
],
|
|
325
|
+
"description": "Routes submissions into the existing PortalLeads pipeline.",
|
|
326
|
+
"properties": {
|
|
327
|
+
"kind": {
|
|
328
|
+
"type": "string",
|
|
329
|
+
"enum": [
|
|
330
|
+
"lead"
|
|
331
|
+
]
|
|
332
|
+
},
|
|
333
|
+
"category": {
|
|
334
|
+
"type": "string",
|
|
335
|
+
"description": "Service category id used to route the lead."
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
"PortalFormSubmissionEmailConfig": {
|
|
340
|
+
"type": "object",
|
|
341
|
+
"required": [
|
|
342
|
+
"kind",
|
|
343
|
+
"to"
|
|
344
|
+
],
|
|
345
|
+
"description": "Routes submissions as a notification email to the listed recipients.",
|
|
346
|
+
"properties": {
|
|
347
|
+
"kind": {
|
|
348
|
+
"type": "string",
|
|
349
|
+
"enum": [
|
|
350
|
+
"email"
|
|
351
|
+
]
|
|
352
|
+
},
|
|
353
|
+
"to": {
|
|
354
|
+
"type": "array",
|
|
355
|
+
"minItems": 1,
|
|
356
|
+
"items": {
|
|
357
|
+
"type": "string",
|
|
358
|
+
"format": "email"
|
|
359
|
+
},
|
|
360
|
+
"description": "Recipient email addresses."
|
|
361
|
+
},
|
|
362
|
+
"subjectTemplate": {
|
|
363
|
+
"type": "string",
|
|
364
|
+
"description": "Optional subject-line template. Use `{title}` to interpolate\nthe form's title; values are intentionally not interpolated\ninto the subject line for compliance reasons.\n",
|
|
365
|
+
"maxLength": 200
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
"PortalFormSubmissionWebhookConfig": {
|
|
370
|
+
"type": "object",
|
|
371
|
+
"required": [
|
|
372
|
+
"kind",
|
|
373
|
+
"url"
|
|
374
|
+
],
|
|
375
|
+
"description": "Posts the submission JSON to a third-party webhook.",
|
|
376
|
+
"properties": {
|
|
377
|
+
"kind": {
|
|
378
|
+
"type": "string",
|
|
379
|
+
"enum": [
|
|
380
|
+
"webhook"
|
|
381
|
+
]
|
|
382
|
+
},
|
|
383
|
+
"url": {
|
|
384
|
+
"type": "string",
|
|
385
|
+
"format": "uri",
|
|
386
|
+
"description": "HTTPS endpoint receiving the signed POST."
|
|
387
|
+
},
|
|
388
|
+
"signingSecretRef": {
|
|
389
|
+
"type": "string",
|
|
390
|
+
"description": "Name of the Key Vault secret used to HMAC-sign the request.\nThe secret value is never returned in API responses.\n"
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
"PortalFormSubmissionConfig": {
|
|
395
|
+
"description": "Discriminated union of submission destinations. Exactly one of\n`lead`, `email`, or `webhook` shapes is used based on `kind`.\n",
|
|
396
|
+
"oneOf": [
|
|
397
|
+
{
|
|
398
|
+
"$ref": "#/definitions/PortalFormSubmissionLeadConfig"
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
"$ref": "#/definitions/PortalFormSubmissionEmailConfig"
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
"$ref": "#/definitions/PortalFormSubmissionWebhookConfig"
|
|
405
|
+
}
|
|
406
|
+
],
|
|
407
|
+
"discriminator": {
|
|
408
|
+
"propertyName": "kind",
|
|
409
|
+
"mapping": {
|
|
410
|
+
"lead": "#/definitions/PortalFormSubmissionLeadConfig",
|
|
411
|
+
"email": "#/definitions/PortalFormSubmissionEmailConfig",
|
|
412
|
+
"webhook": "#/definitions/PortalFormSubmissionWebhookConfig"
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
"PortalFormSummary": {
|
|
417
|
+
"type": "object",
|
|
418
|
+
"description": "List-row projection of a form definition.",
|
|
419
|
+
"required": [
|
|
420
|
+
"formId",
|
|
421
|
+
"title",
|
|
422
|
+
"fieldCount"
|
|
423
|
+
],
|
|
424
|
+
"properties": {
|
|
425
|
+
"formId": {
|
|
426
|
+
"type": "string",
|
|
427
|
+
"description": "Kebab-case identifier.",
|
|
428
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$",
|
|
429
|
+
"example": "contact"
|
|
430
|
+
},
|
|
431
|
+
"title": {
|
|
432
|
+
"type": "string",
|
|
433
|
+
"description": "Human-readable title.",
|
|
434
|
+
"example": "Contact Us"
|
|
435
|
+
},
|
|
436
|
+
"description": {
|
|
437
|
+
"type": "string",
|
|
438
|
+
"description": "Optional descriptive text."
|
|
439
|
+
},
|
|
440
|
+
"fieldCount": {
|
|
441
|
+
"type": "integer",
|
|
442
|
+
"minimum": 0,
|
|
443
|
+
"description": "Number of input-bearing fields in the form.",
|
|
444
|
+
"example": 4
|
|
445
|
+
},
|
|
446
|
+
"attachedPageSlugs": {
|
|
447
|
+
"type": "array",
|
|
448
|
+
"description": "Slugs of pages that currently reference this form via `formId`.",
|
|
449
|
+
"items": {
|
|
450
|
+
"type": "string"
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
"submissionKind": {
|
|
454
|
+
"type": "string",
|
|
455
|
+
"enum": [
|
|
456
|
+
"lead",
|
|
457
|
+
"email",
|
|
458
|
+
"webhook"
|
|
459
|
+
],
|
|
460
|
+
"description": "Submission destination kind."
|
|
461
|
+
},
|
|
462
|
+
"lastUpdated": {
|
|
463
|
+
"type": "string",
|
|
464
|
+
"format": "date-time",
|
|
465
|
+
"description": "Last modification timestamp."
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
"PortalCreateFormRequest": {
|
|
470
|
+
"type": "object",
|
|
471
|
+
"description": "Payload for creating a new form definition.",
|
|
472
|
+
"required": [
|
|
473
|
+
"formId",
|
|
474
|
+
"title",
|
|
475
|
+
"submission",
|
|
476
|
+
"fields"
|
|
477
|
+
],
|
|
478
|
+
"properties": {
|
|
479
|
+
"formId": {
|
|
480
|
+
"type": "string",
|
|
481
|
+
"description": "Kebab-case identifier, unique per site.",
|
|
482
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$",
|
|
483
|
+
"minLength": 1,
|
|
484
|
+
"maxLength": 80
|
|
485
|
+
},
|
|
486
|
+
"title": {
|
|
487
|
+
"type": "string",
|
|
488
|
+
"minLength": 1,
|
|
489
|
+
"maxLength": 200
|
|
490
|
+
},
|
|
491
|
+
"description": {
|
|
492
|
+
"type": "string",
|
|
493
|
+
"maxLength": 2000
|
|
494
|
+
},
|
|
495
|
+
"submitLabel": {
|
|
496
|
+
"type": "string",
|
|
497
|
+
"maxLength": 80
|
|
498
|
+
},
|
|
499
|
+
"successMessage": {
|
|
500
|
+
"type": "string",
|
|
501
|
+
"maxLength": 2000
|
|
502
|
+
},
|
|
503
|
+
"submission": {
|
|
504
|
+
"$ref": "#/definitions/PortalFormSubmissionConfig"
|
|
505
|
+
},
|
|
506
|
+
"steps": {
|
|
507
|
+
"type": "array",
|
|
508
|
+
"items": {
|
|
509
|
+
"$ref": "#/definitions/PortalFormStep"
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
"fields": {
|
|
513
|
+
"type": "array",
|
|
514
|
+
"items": {
|
|
515
|
+
"$ref": "#/definitions/PortalFormField"
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
"PortalUpdateFormRequest": {
|
|
521
|
+
"type": "object",
|
|
522
|
+
"description": "Payload for updating an existing form definition. The `formId`\nis taken from the URL; supplying it in the body is optional and,\nif present, must match the path.\n",
|
|
523
|
+
"properties": {
|
|
524
|
+
"formId": {
|
|
525
|
+
"type": "string",
|
|
526
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$",
|
|
527
|
+
"minLength": 1,
|
|
528
|
+
"maxLength": 80,
|
|
529
|
+
"description": "Optional echo of the path formId; must match if provided."
|
|
530
|
+
},
|
|
531
|
+
"title": {
|
|
532
|
+
"type": "string",
|
|
533
|
+
"minLength": 1,
|
|
534
|
+
"maxLength": 200
|
|
535
|
+
},
|
|
536
|
+
"description": {
|
|
537
|
+
"type": "string",
|
|
538
|
+
"maxLength": 2000
|
|
539
|
+
},
|
|
540
|
+
"submitLabel": {
|
|
541
|
+
"type": "string",
|
|
542
|
+
"maxLength": 80
|
|
543
|
+
},
|
|
544
|
+
"successMessage": {
|
|
545
|
+
"type": "string",
|
|
546
|
+
"maxLength": 2000
|
|
547
|
+
},
|
|
548
|
+
"submission": {
|
|
549
|
+
"$ref": "#/definitions/PortalFormSubmissionConfig"
|
|
550
|
+
},
|
|
551
|
+
"steps": {
|
|
552
|
+
"type": "array",
|
|
553
|
+
"items": {
|
|
554
|
+
"$ref": "#/definitions/PortalFormStep"
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
"fields": {
|
|
558
|
+
"type": "array",
|
|
559
|
+
"items": {
|
|
560
|
+
"$ref": "#/definitions/PortalFormField"
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
"expectedVersion": {
|
|
564
|
+
"type": "integer",
|
|
565
|
+
"minimum": 1,
|
|
566
|
+
"description": "Optional optimistic-concurrency token. When supplied, the\nupdate is rejected with 409 if the stored form's `version`\nno longer matches.\n"
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
"PortalDuplicateFormRequest": {
|
|
571
|
+
"type": "object",
|
|
572
|
+
"description": "Payload for cloning an existing form under a new formId.",
|
|
573
|
+
"required": [
|
|
574
|
+
"formId"
|
|
575
|
+
],
|
|
576
|
+
"properties": {
|
|
577
|
+
"formId": {
|
|
578
|
+
"type": "string",
|
|
579
|
+
"description": "Target kebab-case formId for the cloned form.",
|
|
580
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$",
|
|
581
|
+
"minLength": 1,
|
|
582
|
+
"maxLength": 80
|
|
583
|
+
},
|
|
584
|
+
"title": {
|
|
585
|
+
"type": "string",
|
|
586
|
+
"description": "Optional title override; defaults to `<source title> (copy)`.",
|
|
587
|
+
"minLength": 1,
|
|
588
|
+
"maxLength": 200
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
},
|
|
592
|
+
"PortalFormResponse": {
|
|
593
|
+
"type": "object",
|
|
594
|
+
"description": "Full form definition response.",
|
|
595
|
+
"required": [
|
|
596
|
+
"form"
|
|
597
|
+
],
|
|
598
|
+
"properties": {
|
|
599
|
+
"form": {
|
|
600
|
+
"$ref": "#/definitions/PortalFormDefinition"
|
|
601
|
+
},
|
|
602
|
+
"attachedPageSlugs": {
|
|
603
|
+
"type": "array",
|
|
604
|
+
"description": "Slugs of pages that currently reference this form via `formId`.",
|
|
605
|
+
"items": {
|
|
606
|
+
"type": "string"
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
"PortalFormListResponse": {
|
|
612
|
+
"type": "object",
|
|
613
|
+
"required": [
|
|
614
|
+
"forms",
|
|
615
|
+
"totalCount"
|
|
616
|
+
],
|
|
617
|
+
"properties": {
|
|
618
|
+
"forms": {
|
|
619
|
+
"type": "array",
|
|
620
|
+
"items": {
|
|
621
|
+
"$ref": "#/definitions/PortalFormSummary"
|
|
622
|
+
}
|
|
623
|
+
},
|
|
624
|
+
"totalCount": {
|
|
625
|
+
"type": "integer",
|
|
626
|
+
"minimum": 0,
|
|
627
|
+
"description": "Total number of forms matching the query.",
|
|
628
|
+
"example": 3
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates a loaded form definition against the form-definition JSON
|
|
3
|
+
* Schema emitted by `@dcs/contracts`. Dev-only — failures are logged
|
|
4
|
+
* via `console.warn` rather than thrown so a malformed YAML never
|
|
5
|
+
* takes down a customer site in production.
|
|
6
|
+
*
|
|
7
|
+
* Schema is bundled as a snapshot of
|
|
8
|
+
* `contracts/dist/form-definition.schema.json` to avoid a runtime
|
|
9
|
+
* dependency on the contracts build output.
|
|
10
|
+
*/
|
|
11
|
+
import Ajv, { type ErrorObject, type ValidateFunction } from 'ajv'
|
|
12
|
+
import addFormats from 'ajv-formats'
|
|
13
|
+
import schema from './form-definition.schema.json'
|
|
14
|
+
import type { PortalFormDefinition } from '../types'
|
|
15
|
+
|
|
16
|
+
let validator: ValidateFunction | null = null
|
|
17
|
+
|
|
18
|
+
function getValidator(): ValidateFunction {
|
|
19
|
+
if (validator) return validator
|
|
20
|
+
const ajv = new Ajv({
|
|
21
|
+
allErrors: true,
|
|
22
|
+
strict: false,
|
|
23
|
+
discriminator: false,
|
|
24
|
+
})
|
|
25
|
+
addFormats(ajv)
|
|
26
|
+
validator = ajv.compile(schema as object)
|
|
27
|
+
return validator
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SchemaValidationResult {
|
|
31
|
+
valid: boolean
|
|
32
|
+
errors: ErrorObject[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function validateFormDefinition(
|
|
36
|
+
def: PortalFormDefinition | unknown,
|
|
37
|
+
): SchemaValidationResult {
|
|
38
|
+
const v = getValidator()
|
|
39
|
+
const valid = v(def) as boolean
|
|
40
|
+
return { valid, errors: valid ? [] : (v.errors ?? []) }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Logs a `console.warn` when invalid, but only when `import.meta.env.DEV`. */
|
|
44
|
+
export function warnIfInvalid(
|
|
45
|
+
formId: string,
|
|
46
|
+
def: PortalFormDefinition | unknown,
|
|
47
|
+
isDev: boolean,
|
|
48
|
+
): SchemaValidationResult {
|
|
49
|
+
const result = validateFormDefinition(def)
|
|
50
|
+
if (!result.valid && isDev) {
|
|
51
|
+
// eslint-disable-next-line no-console
|
|
52
|
+
console.warn(
|
|
53
|
+
`[@duffcloudservices/site-forms] Form "${formId}" failed schema validation:`,
|
|
54
|
+
result.errors,
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
return result
|
|
58
|
+
}
|
package/src/shims.d.ts
ADDED