@duffcloudservices/site-forms 0.1.2 → 0.1.3

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.
@@ -6,7 +6,6 @@
6
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
7
  "required": [
8
8
  "formId",
9
- "title",
10
9
  "submission",
11
10
  "fields"
12
11
  ],
@@ -19,17 +18,8 @@
19
18
  "maxLength": 80,
20
19
  "example": "contact"
21
20
  },
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
21
+ "formKind": {
22
+ "$ref": "#/definitions/PortalFormKind"
33
23
  },
34
24
  "submitLabel": {
35
25
  "type": "string",
@@ -46,6 +36,9 @@
46
36
  "submission": {
47
37
  "$ref": "#/definitions/PortalFormSubmissionConfig"
48
38
  },
39
+ "attachmentPolicy": {
40
+ "$ref": "#/definitions/PortalFormAttachmentPolicy"
41
+ },
49
42
  "steps": {
50
43
  "type": "array",
51
44
  "description": "Optional multi-step grouping. When omitted, fields render as a single page.",
@@ -77,6 +70,278 @@
77
70
  }
78
71
  },
79
72
  "definitions": {
73
+ "PortalFormKind": {
74
+ "type": "string",
75
+ "description": "High-level form kind used by the visual page editor and submission\npipeline. `freeform` preserves fully editable questionnaires; the\nstandard values provide predictable field roles and routing for common\nsite forms.\n",
76
+ "enum": [
77
+ "freeform",
78
+ "contact",
79
+ "revenue-contractor",
80
+ "resume-submission"
81
+ ],
82
+ "default": "freeform"
83
+ },
84
+ "PortalFormFieldRole": {
85
+ "type": "string",
86
+ "description": "Semantic role for a field inside a standard form. Free-form forms may\nomit roles or use `custom`; standard forms use roles so submissions can\nbe surfaced consistently as contact messages, notifications, revenue\ncontractor inquiries, or resume submissions without locking the editor\nout of label/help-text changes.\n",
87
+ "enum": [
88
+ "contact-name",
89
+ "contact-email",
90
+ "contact-phone",
91
+ "contact-company",
92
+ "subject",
93
+ "message",
94
+ "summary",
95
+ "image-attachments",
96
+ "resume",
97
+ "consent",
98
+ "custom"
99
+ ]
100
+ },
101
+ "PortalFormAttachmentPolicy": {
102
+ "type": "object",
103
+ "description": "Attachment expectations for standard forms. For\n`revenue-contractor`, image attachments are expected so contractors can\ninclude project photos. For `resume-submission`, the resume field\nshould accept document uploads. Free-form questionnaires can omit this.\n",
104
+ "properties": {
105
+ "expected": {
106
+ "type": "boolean",
107
+ "default": false,
108
+ "description": "Whether the standard flow should prompt for attachments."
109
+ },
110
+ "required": {
111
+ "type": "boolean",
112
+ "default": false,
113
+ "description": "Whether at least one attachment is required."
114
+ },
115
+ "maxFiles": {
116
+ "type": "integer",
117
+ "minimum": 1,
118
+ "maximum": 20,
119
+ "description": "Maximum number of files accepted.",
120
+ "example": 5
121
+ },
122
+ "maxFileSizeBytes": {
123
+ "type": "integer",
124
+ "minimum": 1,
125
+ "description": "Maximum size per file in bytes.",
126
+ "example": 10485760
127
+ },
128
+ "accept": {
129
+ "type": "array",
130
+ "description": "Accepted MIME types or extensions (for example `image/*`).",
131
+ "items": {
132
+ "type": "string"
133
+ }
134
+ }
135
+ }
136
+ },
137
+ "PublicSiteFormContact": {
138
+ "type": "object",
139
+ "description": "Normalized contact identity supplied with a public form submission.",
140
+ "properties": {
141
+ "name": {
142
+ "type": "string",
143
+ "maxLength": 160
144
+ },
145
+ "email": {
146
+ "type": "string",
147
+ "format": "email",
148
+ "maxLength": 255
149
+ },
150
+ "phone": {
151
+ "type": "string",
152
+ "maxLength": 32
153
+ },
154
+ "company": {
155
+ "type": "string",
156
+ "maxLength": 160
157
+ }
158
+ }
159
+ },
160
+ "PublicSiteFormAttachment": {
161
+ "type": "object",
162
+ "description": "Metadata for an attachment associated with a public form submission.",
163
+ "required": [
164
+ "fileName"
165
+ ],
166
+ "properties": {
167
+ "fileName": {
168
+ "type": "string",
169
+ "maxLength": 255
170
+ },
171
+ "contentType": {
172
+ "type": "string",
173
+ "maxLength": 120
174
+ },
175
+ "sizeBytes": {
176
+ "type": "integer",
177
+ "minimum": 0
178
+ },
179
+ "storageUrl": {
180
+ "type": "string",
181
+ "format": "uri",
182
+ "description": "Internal or pre-uploaded storage URL when the binary was uploaded separately."
183
+ }
184
+ }
185
+ },
186
+ "PublicSiteFormSubmissionRequest": {
187
+ "type": "object",
188
+ "description": "JSON request for public managed-form submissions.",
189
+ "required": [
190
+ "values"
191
+ ],
192
+ "properties": {
193
+ "formKind": {
194
+ "$ref": "#/definitions/PortalFormKind"
195
+ },
196
+ "pageSlug": {
197
+ "type": "string",
198
+ "description": "Optional visual-editor page slug where the form was rendered."
199
+ },
200
+ "contact": {
201
+ "$ref": "#/definitions/PublicSiteFormContact"
202
+ },
203
+ "subject": {
204
+ "type": "string",
205
+ "maxLength": 200,
206
+ "description": "Contact subject or inquiry title."
207
+ },
208
+ "summary": {
209
+ "type": "string",
210
+ "maxLength": 4000,
211
+ "description": "Project, request, or applicant summary for standard flows."
212
+ },
213
+ "message": {
214
+ "type": "string",
215
+ "maxLength": 4000,
216
+ "description": "Free-text message supplied by the submitter."
217
+ },
218
+ "values": {
219
+ "type": "object",
220
+ "additionalProperties": true,
221
+ "description": "Field id to submitted value map for the attached PortalFormDefinition."
222
+ },
223
+ "attachments": {
224
+ "type": "array",
225
+ "description": "Attachment metadata for JSON submissions.",
226
+ "items": {
227
+ "$ref": "#/definitions/PublicSiteFormAttachment"
228
+ }
229
+ },
230
+ "source": {
231
+ "type": "string",
232
+ "maxLength": 120,
233
+ "description": "Capture source such as `site-contact-page` or `visual-page-editor`."
234
+ },
235
+ "consent": {
236
+ "type": "boolean",
237
+ "description": "Whether the submitter consented to follow-up."
238
+ }
239
+ }
240
+ },
241
+ "PublicSiteFormMultipartSubmissionRequest": {
242
+ "type": "object",
243
+ "description": "Multipart request for public managed-form submissions with binary uploads.",
244
+ "properties": {
245
+ "formKind": {
246
+ "$ref": "#/definitions/PortalFormKind"
247
+ },
248
+ "pageSlug": {
249
+ "type": "string"
250
+ },
251
+ "values": {
252
+ "type": "string",
253
+ "description": "JSON-encoded field id to submitted value map."
254
+ },
255
+ "name": {
256
+ "type": "string",
257
+ "maxLength": 160
258
+ },
259
+ "email": {
260
+ "type": "string",
261
+ "format": "email",
262
+ "maxLength": 255
263
+ },
264
+ "phone": {
265
+ "type": "string",
266
+ "maxLength": 32
267
+ },
268
+ "company": {
269
+ "type": "string",
270
+ "maxLength": 160
271
+ },
272
+ "subject": {
273
+ "type": "string",
274
+ "maxLength": 200
275
+ },
276
+ "summary": {
277
+ "type": "string",
278
+ "maxLength": 4000
279
+ },
280
+ "message": {
281
+ "type": "string",
282
+ "maxLength": 4000
283
+ },
284
+ "attachments": {
285
+ "type": "array",
286
+ "description": "Image or supporting files for standard form submissions.",
287
+ "items": {
288
+ "type": "string",
289
+ "format": "binary"
290
+ }
291
+ },
292
+ "resume": {
293
+ "type": "string",
294
+ "format": "binary",
295
+ "description": "Resume document for `resume-submission` standard forms."
296
+ },
297
+ "source": {
298
+ "type": "string",
299
+ "maxLength": 120
300
+ },
301
+ "consent": {
302
+ "type": "boolean"
303
+ }
304
+ }
305
+ },
306
+ "PublicSiteFormSubmissionResponse": {
307
+ "type": "object",
308
+ "required": [
309
+ "id",
310
+ "status",
311
+ "submittedAt"
312
+ ],
313
+ "properties": {
314
+ "id": {
315
+ "type": "string",
316
+ "description": "Unique submission identifier."
317
+ },
318
+ "status": {
319
+ "type": "string",
320
+ "enum": [
321
+ "submitted",
322
+ "accepted",
323
+ "queued"
324
+ ]
325
+ },
326
+ "submittedAt": {
327
+ "type": "string",
328
+ "format": "date-time"
329
+ },
330
+ "message": {
331
+ "type": "string",
332
+ "description": "Friendly acknowledgement shown to the submitter."
333
+ },
334
+ "contactMessageId": {
335
+ "type": "string",
336
+ "nullable": true,
337
+ "description": "Future portal contact-message identifier when surfaced in the portal."
338
+ },
339
+ "notificationQueued": {
340
+ "type": "boolean",
341
+ "description": "Whether a portal notification/contact-message handoff was queued."
342
+ }
343
+ }
344
+ },
80
345
  "PortalFormFieldType": {
81
346
  "type": "string",
82
347
  "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",
@@ -206,6 +471,9 @@
206
471
  "type": {
207
472
  "$ref": "#/definitions/PortalFormFieldType"
208
473
  },
474
+ "role": {
475
+ "$ref": "#/definitions/PortalFormFieldRole"
476
+ },
209
477
  "label": {
210
478
  "type": "string",
211
479
  "description": "Human-readable label rendered above the input.",
@@ -361,7 +629,7 @@
361
629
  },
362
630
  "subjectTemplate": {
363
631
  "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",
632
+ "description": "Optional subject-line template. Submitted values are intentionally\nnot interpolated into the subject line for compliance reasons.\n",
365
633
  "maxLength": 200
366
634
  }
367
635
  }
@@ -418,7 +686,6 @@
418
686
  "description": "List-row projection of a form definition.",
419
687
  "required": [
420
688
  "formId",
421
- "title",
422
689
  "fieldCount"
423
690
  ],
424
691
  "properties": {
@@ -428,14 +695,8 @@
428
695
  "pattern": "^[a-z0-9][a-z0-9-]*$",
429
696
  "example": "contact"
430
697
  },
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."
698
+ "formKind": {
699
+ "$ref": "#/definitions/PortalFormKind"
439
700
  },
440
701
  "fieldCount": {
441
702
  "type": "integer",
@@ -471,7 +732,6 @@
471
732
  "description": "Payload for creating a new form definition.",
472
733
  "required": [
473
734
  "formId",
474
- "title",
475
735
  "submission",
476
736
  "fields"
477
737
  ],
@@ -483,14 +743,8 @@
483
743
  "minLength": 1,
484
744
  "maxLength": 80
485
745
  },
486
- "title": {
487
- "type": "string",
488
- "minLength": 1,
489
- "maxLength": 200
490
- },
491
- "description": {
492
- "type": "string",
493
- "maxLength": 2000
746
+ "formKind": {
747
+ "$ref": "#/definitions/PortalFormKind"
494
748
  },
495
749
  "submitLabel": {
496
750
  "type": "string",
@@ -503,6 +757,9 @@
503
757
  "submission": {
504
758
  "$ref": "#/definitions/PortalFormSubmissionConfig"
505
759
  },
760
+ "attachmentPolicy": {
761
+ "$ref": "#/definitions/PortalFormAttachmentPolicy"
762
+ },
506
763
  "steps": {
507
764
  "type": "array",
508
765
  "items": {
@@ -528,14 +785,8 @@
528
785
  "maxLength": 80,
529
786
  "description": "Optional echo of the path formId; must match if provided."
530
787
  },
531
- "title": {
532
- "type": "string",
533
- "minLength": 1,
534
- "maxLength": 200
535
- },
536
- "description": {
537
- "type": "string",
538
- "maxLength": 2000
788
+ "formKind": {
789
+ "$ref": "#/definitions/PortalFormKind"
539
790
  },
540
791
  "submitLabel": {
541
792
  "type": "string",
@@ -548,6 +799,9 @@
548
799
  "submission": {
549
800
  "$ref": "#/definitions/PortalFormSubmissionConfig"
550
801
  },
802
+ "attachmentPolicy": {
803
+ "$ref": "#/definitions/PortalFormAttachmentPolicy"
804
+ },
551
805
  "steps": {
552
806
  "type": "array",
553
807
  "items": {
@@ -580,12 +834,6 @@
580
834
  "pattern": "^[a-z0-9][a-z0-9-]*$",
581
835
  "minLength": 1,
582
836
  "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
837
  }
590
838
  }
591
839
  },
@@ -628,6 +876,123 @@
628
876
  "example": 3
629
877
  }
630
878
  }
879
+ },
880
+ "PortalFormSubmission": {
881
+ "type": "object",
882
+ "description": "A single recorded submission against a managed form. PHI fields are\nreplaced with `[REDACTED]` for Medical-category sites, in which case\n`redactedForPhi` is true.\n",
883
+ "required": [
884
+ "id",
885
+ "formId",
886
+ "siteSlug",
887
+ "submittedAt",
888
+ "source",
889
+ "values",
890
+ "redactedForPhi"
891
+ ],
892
+ "properties": {
893
+ "id": {
894
+ "type": "string",
895
+ "description": "Unique submission row key (reverse-chronological)."
896
+ },
897
+ "formId": {
898
+ "type": "string"
899
+ },
900
+ "siteSlug": {
901
+ "type": "string"
902
+ },
903
+ "formKind": {
904
+ "$ref": "#/definitions/PortalFormKind"
905
+ },
906
+ "submittedAt": {
907
+ "type": "string",
908
+ "format": "date-time"
909
+ },
910
+ "source": {
911
+ "type": "string",
912
+ "description": "Capture channel (e.g. `web`, `import`)."
913
+ },
914
+ "values": {
915
+ "type": "object",
916
+ "additionalProperties": true,
917
+ "description": "Field id → submitted value map. PHI values are redacted."
918
+ },
919
+ "attachments": {
920
+ "type": "array",
921
+ "description": "Attachment metadata captured with the submission.",
922
+ "items": {
923
+ "$ref": "#/definitions/PublicSiteFormAttachment"
924
+ }
925
+ },
926
+ "leadId": {
927
+ "type": "string",
928
+ "nullable": true,
929
+ "description": "Cross-reference to the lead row created by the legacy capture flow."
930
+ },
931
+ "contactMessageId": {
932
+ "type": "string",
933
+ "nullable": true,
934
+ "description": "Future portal contact-message identifier when surfaced in the portal."
935
+ },
936
+ "redactedForPhi": {
937
+ "type": "boolean",
938
+ "description": "True when one or more values were replaced with the PHI placeholder."
939
+ },
940
+ "remoteIp": {
941
+ "type": "string",
942
+ "nullable": true
943
+ },
944
+ "userAgent": {
945
+ "type": "string",
946
+ "nullable": true
947
+ }
948
+ }
949
+ },
950
+ "PortalFormSubmissionSummary": {
951
+ "type": "object",
952
+ "description": "List-view representation of a submission.",
953
+ "required": [
954
+ "id",
955
+ "formId",
956
+ "submittedAt",
957
+ "redactedForPhi",
958
+ "summary"
959
+ ],
960
+ "properties": {
961
+ "id": {
962
+ "type": "string"
963
+ },
964
+ "formId": {
965
+ "type": "string"
966
+ },
967
+ "formKind": {
968
+ "$ref": "#/definitions/PortalFormKind"
969
+ },
970
+ "submittedAt": {
971
+ "type": "string",
972
+ "format": "date-time"
973
+ },
974
+ "redactedForPhi": {
975
+ "type": "boolean"
976
+ },
977
+ "summary": {
978
+ "type": "string",
979
+ "description": "Short single-line preview suitable for the portal list view."
980
+ }
981
+ }
982
+ },
983
+ "PortalFormSubmissionListResponse": {
984
+ "type": "object",
985
+ "required": [
986
+ "submissions"
987
+ ],
988
+ "properties": {
989
+ "submissions": {
990
+ "type": "array",
991
+ "items": {
992
+ "$ref": "#/definitions/PortalFormSubmissionSummary"
993
+ }
994
+ }
995
+ }
631
996
  }
632
997
  }
633
998
  }
package/src/style.css CHANGED
@@ -91,6 +91,41 @@
91
91
  resize: vertical;
92
92
  }
93
93
 
94
+ .dcs-form-file {
95
+ min-height: 3.25rem;
96
+ padding: 0.5rem 0.75rem;
97
+ cursor: pointer;
98
+ }
99
+
100
+ .dcs-form-file::file-selector-button {
101
+ margin: 0 0.85rem 0 0;
102
+ border: 0;
103
+ border-radius: 9999px;
104
+ padding: 0.65rem 1rem;
105
+ font: inherit;
106
+ font-weight: 700;
107
+ line-height: 1;
108
+ color: #fff;
109
+ background: linear-gradient(135deg, #1d4ed8, #2563eb);
110
+ box-shadow: 0 8px 18px rgba(37, 99, 235, 0.2);
111
+ cursor: pointer;
112
+ transition:
113
+ transform 0.15s ease,
114
+ box-shadow 0.15s ease,
115
+ opacity 0.15s ease;
116
+ }
117
+
118
+ .dcs-form-file:hover::file-selector-button {
119
+ transform: translateY(-1px);
120
+ box-shadow: 0 10px 22px rgba(37, 99, 235, 0.26);
121
+ }
122
+
123
+ .dcs-form-file:disabled,
124
+ .dcs-form-file:disabled::file-selector-button {
125
+ cursor: not-allowed;
126
+ opacity: 0.7;
127
+ }
128
+
94
129
  .dcs-form-select {
95
130
  appearance: none;
96
131
  }
package/src/types.ts CHANGED
@@ -32,6 +32,29 @@ export type PortalFormFieldType =
32
32
 
33
33
  export type PortalFormFieldWidth = 'full' | 'half' | 'third'
34
34
 
35
+ export type PortalFormKind = 'freeform' | 'contact' | 'revenue-contractor' | 'resume-submission'
36
+
37
+ export type PortalFormFieldRole =
38
+ | 'custom'
39
+ | 'contact-name'
40
+ | 'contact-email'
41
+ | 'contact-phone'
42
+ | 'contact-company'
43
+ | 'subject'
44
+ | 'message'
45
+ | 'summary'
46
+ | 'image-attachments'
47
+ | 'resume'
48
+ | 'consent'
49
+
50
+ export interface PortalFormAttachmentPolicy {
51
+ expected?: boolean
52
+ required?: boolean
53
+ maxFiles?: number
54
+ maxFileSizeBytes?: number
55
+ accept?: string[]
56
+ }
57
+
35
58
  export interface PortalFormFieldOption {
36
59
  value: string
37
60
  label: string
@@ -59,6 +82,7 @@ export interface PortalFormField {
59
82
  placeholder?: string
60
83
  defaultValue?: string | number | boolean | string[]
61
84
  required?: boolean
85
+ role?: PortalFormFieldRole
62
86
  width?: PortalFormFieldWidth
63
87
  options?: PortalFormFieldOption[]
64
88
  validation?: PortalFormFieldValidation
@@ -104,11 +128,11 @@ export type PortalFormSubmissionConfig =
104
128
 
105
129
  export interface PortalFormDefinition {
106
130
  formId: string
107
- title: string
108
- description?: string
131
+ formKind?: PortalFormKind
109
132
  submitLabel?: string
110
133
  successMessage?: string
111
134
  submission: PortalFormSubmissionConfig
135
+ attachmentPolicy?: PortalFormAttachmentPolicy
112
136
  steps?: PortalFormStep[]
113
137
  fields: PortalFormField[]
114
138
  version?: number