@10x-media/form-builder 0.1.0-beta.1 → 0.1.0-beta.2
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/CHANGELOG.md +16 -0
- package/README.md +3 -1
- package/dist/collections/formSubmissions.js +57 -38
- package/dist/collections/formSubmissions.js.map +1 -1
- package/dist/collections/forms.js +125 -114
- package/dist/collections/forms.js.map +1 -1
- package/dist/collections/uploads.d.ts +2 -1
- package/dist/collections/uploads.js +22 -13
- package/dist/collections/uploads.js.map +1 -1
- package/dist/exports/react.d.ts +2 -2
- package/dist/exports/react.js +1 -1
- package/dist/exports/types.d.ts +2 -1
- package/dist/fields/buildFieldBlocks.js +14 -0
- package/dist/fields/buildFieldBlocks.js.map +1 -1
- package/dist/fields/builtin/index.js +3 -1
- package/dist/fields/builtin/index.js.map +1 -1
- package/dist/fields/builtin/repeater.js +44 -0
- package/dist/fields/builtin/repeater.js.map +1 -0
- package/dist/fields/types.d.ts +2 -1
- package/dist/index.d.ts +19 -0
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/plugin/collectionOverrides.d.ts +22 -0
- package/dist/plugin/registerCollections.js +37 -9
- package/dist/plugin/registerCollections.js.map +1 -1
- package/dist/react/Form.d.ts +32 -3
- package/dist/react/Form.js +47 -5
- package/dist/react/Form.js.map +1 -1
- package/dist/react/FormContext.js.map +1 -1
- package/dist/react/renderers/index.js +2 -0
- package/dist/react/renderers/index.js.map +1 -1
- package/dist/react/renderers/repeater.js +113 -0
- package/dist/react/renderers/repeater.js.map +1 -0
- package/dist/submissions/SubmissionAnswers.d.ts +2 -4
- package/dist/submissions/SubmissionAnswers.js +110 -34
- package/dist/submissions/SubmissionAnswers.js.map +1 -1
- package/dist/submissions/SubmissionAnswersClient.js +142 -0
- package/dist/submissions/SubmissionAnswersClient.js.map +1 -0
- package/dist/submissions/runSubmission.js +54 -2
- package/dist/submissions/runSubmission.js.map +1 -1
- package/dist/submissions/types.d.ts +2 -1
- package/dist/translations/en.js +26 -1
- package/dist/translations/en.js.map +1 -1
- package/dist/translations/keys.d.ts +25 -0
- package/dist/translations/keys.js +26 -1
- package/dist/translations/keys.js.map +1 -1
- package/dist/validation/runValidation.js +1 -1
- package/dist/validation/runValidation.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @10x-media/form-builder
|
|
2
2
|
|
|
3
|
+
## 0.1.0-beta.2
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Add `repeater` field type: a field that captures a dynamic list of rows, each containing a set of sub-fields defined once in the admin UI. Includes row-count validation (`minRows`/`maxRows`), a client-side renderer with add/remove row controls, per-row sub-field validation on the server, and a numbered row view in the submission answers panel.
|
|
8
|
+
|
|
9
|
+
Fix `minRows` zero-row bypass: a repeater submitted with no rows was silently accepted even when `minRows > 0`, because the empty-array coercion was immediately skipped by the field loop's empty guard. The guard now lets repeaters fall through to `validate()` so row-count constraints are enforced correctly.
|
|
10
|
+
|
|
11
|
+
Fix sub-field error display: server-side sub-field validation errors (reported with path `fieldName[rowIndex].subFieldName`) are now surfaced inline next to the offending input in the repeater renderer. Client-side sub-field validation also runs on submit so errors appear before the request is sent.
|
|
12
|
+
|
|
13
|
+
Replace `deepMerge`-based collection overrides with an explicit spread API. The `overrides` plugin option now accepts `CollectionOverrides` objects for `forms`, `formSubmissions`, and `uploads`. Fields are overridden via a `FieldsOverride` function (`({ defaultFields }) => Field[]`) that receives the plugin's defaults and returns the final array, making additions and removals intentional. Hooks are appended after the plugin's own hooks, guaranteeing that the spam guard and submission validator always run first. Spread order per key is documented and encodes who wins without relying on a merge library.
|
|
14
|
+
|
|
15
|
+
Add `renderSubmit`, `renderNext`, and `renderBack` render props to `<Form>` for replacing the default control buttons with custom components. Also add `submitButtonClassName`, `nextButtonClassName`, and `backButtonClassName` for styling the default buttons without replacing them.
|
|
16
|
+
|
|
17
|
+
Add `showSubmissionRawFields` plugin option. The submission admin view now renders a formatted `SubmissionAnswers` component (formatted values, repeater rows, consent proofs, metadata) as the primary view. The raw `values`, `descriptors`, and `consent` JSON fields are hidden by default because they are fully represented by this component; set `showSubmissionRawFields: true` to show them.
|
|
18
|
+
|
|
3
19
|
## 0.1.0-beta.1
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Part of the [@10x-media Payload plugins](https://github.com/10x-media/payload-pl
|
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
|
-
- **Fields**: text, textarea, email, number, select, checkbox, file, consent, and
|
|
11
|
+
- **Fields**: text, textarea, email, number, select, checkbox, file, consent, calculation, and repeater (dynamic row lists with per-row sub-field validation), authored as Payload blocks; custom types via `defineFormField` are never second-class.
|
|
12
12
|
- **Validation**: declarative per-field rules, custom messages and severities, cross-field and async server-only rules, a Standard Schema escape hatch (zod, valibot, ...), one server-authoritative engine.
|
|
13
13
|
- **Conditional logic**: `visibleWhen` / `validateWhen` in Payload's `Where` shape, one isomorphic engine on client and server.
|
|
14
14
|
- **Multi-step flows** with conditional branching, authored in the admin flow builder.
|
|
@@ -20,6 +20,7 @@ Part of the [@10x-media Payload plugins](https://github.com/10x-media/payload-pl
|
|
|
20
20
|
- **Consent** with proof by reference and policy-version capture; **file uploads** with server-enforced MIME/size; **polls** with a gated public results endpoint.
|
|
21
21
|
- **Spam protection** on by default: honeypot, per-identity rate limiting, a captcha seam, upload-ownership scoping, privacy-first metadata.
|
|
22
22
|
- **Accessibility** verified by automated axe checks; **typed translations** via `@10x-media/form-builder/i18n`.
|
|
23
|
+
- **Collection overrides**: extend `forms`, `form-submissions`, and `form-uploads` with extra fields, hooks, and access rules using an explicit spread API that guarantees plugin-critical hooks always run.
|
|
23
24
|
|
|
24
25
|
## Quick start
|
|
25
26
|
|
|
@@ -69,6 +70,7 @@ Full documentation at [docs.10xmedia.de](https://docs.10xmedia.de/form-builder):
|
|
|
69
70
|
- [Polls and aggregation](https://docs.10xmedia.de/form-builder/polls)
|
|
70
71
|
- [Spam protection](https://docs.10xmedia.de/form-builder/spam)
|
|
71
72
|
- [i18n](https://docs.10xmedia.de/form-builder/i18n)
|
|
73
|
+
- [Collection overrides](https://docs.10xmedia.de/form-builder/customization)
|
|
72
74
|
|
|
73
75
|
## License
|
|
74
76
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { keys } from "../translations/keys.js";
|
|
2
|
+
import { labelForKey } from "../translations/server.js";
|
|
1
3
|
import { isLoggedIn } from "../plugin/access.js";
|
|
2
4
|
import { dispatchActions } from "../actions/dispatch.js";
|
|
3
5
|
import { resolveEventSink } from "../events/resolveEventSink.js";
|
|
@@ -57,32 +59,8 @@ const makeAfterChange = (args) => async ({ doc, operation, req }) => {
|
|
|
57
59
|
}
|
|
58
60
|
return doc;
|
|
59
61
|
};
|
|
60
|
-
const buildSubmissionsCollection = ({ registry, ruleRegistry, consentRegistry, actionRegistry = /* @__PURE__ */ new Map(), events, hasRunner = false, uploadSlug, spam }) =>
|
|
61
|
-
|
|
62
|
-
labels: {
|
|
63
|
-
singular: "Submission",
|
|
64
|
-
plural: "Submissions"
|
|
65
|
-
},
|
|
66
|
-
admin: { group: "Forms" },
|
|
67
|
-
access: {
|
|
68
|
-
create: () => true,
|
|
69
|
-
read: isLoggedIn,
|
|
70
|
-
update: () => false
|
|
71
|
-
},
|
|
72
|
-
hooks: {
|
|
73
|
-
beforeValidate: [...spam ? [buildSpamGuard(spam)] : [], validateSubmission({
|
|
74
|
-
registry,
|
|
75
|
-
ruleRegistry,
|
|
76
|
-
consentRegistry,
|
|
77
|
-
uploadSlug
|
|
78
|
-
})],
|
|
79
|
-
afterChange: [makeAfterChange({
|
|
80
|
-
actionRegistry,
|
|
81
|
-
events,
|
|
82
|
-
hasRunner
|
|
83
|
-
})]
|
|
84
|
-
},
|
|
85
|
-
fields: [
|
|
62
|
+
const buildSubmissionsCollection = ({ registry, ruleRegistry, consentRegistry, actionRegistry = /* @__PURE__ */ new Map(), events, hasRunner = false, uploadSlug, spam, showRawFields = false, overrides }) => {
|
|
63
|
+
const defaultFields = [
|
|
86
64
|
{
|
|
87
65
|
name: "form",
|
|
88
66
|
type: "relationship",
|
|
@@ -94,10 +72,10 @@ const buildSubmissionsCollection = ({ registry, ruleRegistry, consentRegistry, a
|
|
|
94
72
|
type: "select",
|
|
95
73
|
defaultValue: "complete",
|
|
96
74
|
options: [{
|
|
97
|
-
label:
|
|
75
|
+
label: labelForKey(keys.statusComplete),
|
|
98
76
|
value: "complete"
|
|
99
77
|
}, {
|
|
100
|
-
label:
|
|
78
|
+
label: labelForKey(keys.statusPartial),
|
|
101
79
|
value: "partial"
|
|
102
80
|
}],
|
|
103
81
|
access: {
|
|
@@ -105,33 +83,74 @@ const buildSubmissionsCollection = ({ registry, ruleRegistry, consentRegistry, a
|
|
|
105
83
|
update: isLoggedIn
|
|
106
84
|
}
|
|
107
85
|
},
|
|
86
|
+
{
|
|
87
|
+
name: "answers",
|
|
88
|
+
type: "ui",
|
|
89
|
+
admin: { components: { Field: "@10x-media/form-builder/rsc#SubmissionAnswers" } }
|
|
90
|
+
},
|
|
108
91
|
{
|
|
109
92
|
name: "locale",
|
|
110
93
|
type: "text"
|
|
111
94
|
},
|
|
112
95
|
{
|
|
113
96
|
name: "values",
|
|
114
|
-
type: "json"
|
|
97
|
+
type: "json",
|
|
98
|
+
admin: { hidden: !showRawFields }
|
|
115
99
|
},
|
|
116
100
|
{
|
|
117
101
|
name: "descriptors",
|
|
118
|
-
type: "json"
|
|
102
|
+
type: "json",
|
|
103
|
+
admin: { hidden: !showRawFields }
|
|
119
104
|
},
|
|
120
105
|
{
|
|
121
106
|
name: "consent",
|
|
122
|
-
type: "json"
|
|
107
|
+
type: "json",
|
|
108
|
+
admin: { hidden: !showRawFields }
|
|
123
109
|
},
|
|
124
110
|
{
|
|
125
111
|
name: "meta",
|
|
126
112
|
type: "json"
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
name: "answers",
|
|
130
|
-
type: "ui",
|
|
131
|
-
admin: { components: { Field: "@10x-media/form-builder/rsc#SubmissionAnswers" } }
|
|
132
113
|
}
|
|
133
|
-
]
|
|
134
|
-
|
|
114
|
+
];
|
|
115
|
+
return {
|
|
116
|
+
...overrides ?? {},
|
|
117
|
+
slug: FORM_SUBMISSIONS_SLUG,
|
|
118
|
+
labels: {
|
|
119
|
+
singular: labelForKey(keys.collectionSubmissionSingular),
|
|
120
|
+
plural: labelForKey(keys.collectionSubmissionPlural),
|
|
121
|
+
...overrides?.labels ?? {}
|
|
122
|
+
},
|
|
123
|
+
admin: {
|
|
124
|
+
group: "Forms",
|
|
125
|
+
...overrides?.admin ?? {}
|
|
126
|
+
},
|
|
127
|
+
access: {
|
|
128
|
+
create: () => true,
|
|
129
|
+
read: isLoggedIn,
|
|
130
|
+
update: () => false,
|
|
131
|
+
...overrides?.access ?? {}
|
|
132
|
+
},
|
|
133
|
+
hooks: {
|
|
134
|
+
...overrides?.hooks ?? {},
|
|
135
|
+
beforeValidate: [
|
|
136
|
+
...spam ? [buildSpamGuard(spam)] : [],
|
|
137
|
+
validateSubmission({
|
|
138
|
+
registry,
|
|
139
|
+
ruleRegistry,
|
|
140
|
+
consentRegistry,
|
|
141
|
+
uploadSlug
|
|
142
|
+
}),
|
|
143
|
+
...overrides?.hooks?.beforeValidate ?? []
|
|
144
|
+
],
|
|
145
|
+
afterChange: [makeAfterChange({
|
|
146
|
+
actionRegistry,
|
|
147
|
+
events,
|
|
148
|
+
hasRunner
|
|
149
|
+
}), ...overrides?.hooks?.afterChange ?? []]
|
|
150
|
+
},
|
|
151
|
+
fields: overrides?.fields ? overrides.fields({ defaultFields }) : defaultFields
|
|
152
|
+
};
|
|
153
|
+
};
|
|
135
154
|
//#endregion
|
|
136
155
|
export { FORM_SUBMISSIONS_SLUG, buildSubmissionsCollection };
|
|
137
156
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formSubmissions.js","names":[],"sources":["../../src/collections/formSubmissions.ts"],"sourcesContent":["import type { CollectionAfterChangeHook, CollectionConfig } from 'payload'\nimport { dispatchActions } from '../actions/dispatch'\nimport type { ActionRegistry } from '../actions/registry'\nimport type { ActionInstance } from '../actions/runActions'\nimport type { ConsentSourceRegistry } from '../consent/registry'\nimport { resolveEventSink } from '../events/resolveEventSink'\nimport type { FormEventSink } from '../events/types'\nimport type { FieldTypeRegistry } from '../fields/registry'\nimport { isLoggedIn } from '../plugin/access'\nimport { buildSpamGuard } from '../spam/spamGuard'\nimport type { ResolvedSpamConfig } from '../spam/types'\nimport { validateSubmission } from '../submissions/validateSubmission'\nimport type { ValidationRuleRegistry } from '../validation/registry'\nimport { FORMS_SLUG } from './forms'\n\nexport const FORM_SUBMISSIONS_SLUG = 'form-submissions'\n\ntype BuildSubmissionsCollectionArgs = {\n\tregistry: FieldTypeRegistry\n\truleRegistry: ValidationRuleRegistry\n\tconsentRegistry: ConsentSourceRegistry\n\tactionRegistry?: ActionRegistry\n\tevents?: FormEventSink\n\t/** Whether a job runner is likely present; gates the queued vs bounded-inline dispatch path. */\n\thasRunner?: boolean\n\t/** Upload collection slug for file fields without an explicit `relationTo`. */\n\tuploadSlug?: string\n\t/** Resolved spam config; when active, prepends the spam guard before validation. `false` disables it. */\n\tspam?: ResolvedSpamConfig | false\n}\n\nconst formIdOf = (form: unknown): number | string | undefined => {\n\tif (typeof form === 'number' || typeof form === 'string') {\n\t\treturn form\n\t}\n\tif (form && typeof form === 'object' && 'id' in form) {\n\t\tconst id = (form as { id: unknown }).id\n\t\tif (typeof id === 'number' || typeof id === 'string') {\n\t\t\treturn id\n\t\t}\n\t}\n\treturn undefined\n}\n\n/**\n * On a completed submission create, dispatch the form's post-submit actions and emit `submission.created`.\n * Both are side effects on an already-written row, so the whole body is wrapped: nothing it does can throw\n * past the hook (a failed action or sink must never fail the submission write). Dispatch is itself bounded\n * and non-throwing; the inline fallback is awaited so the bound caps the added latency, while the queued\n * path returns immediately. The form's `actions` are null-guarded for legacy rows created before the field\n * existed.\n */\nconst makeAfterChange =\n\t(args: {\n\t\tactionRegistry: ActionRegistry\n\t\tevents?: FormEventSink\n\t\thasRunner: boolean\n\t}): CollectionAfterChangeHook =>\n\tasync ({ doc, operation, req }) => {\n\t\tif (operation !== 'create' || (doc.status != null && doc.status !== 'complete')) {\n\t\t\treturn doc\n\t\t}\n\t\tconst { payload } = req\n\t\ttry {\n\t\t\tconst formId = formIdOf(doc.form)\n\t\t\tif (formId == null) {\n\t\t\t\treturn doc\n\t\t\t}\n\t\t\tconst form = await payload\n\t\t\t\t.findByID({ collection: FORMS_SLUG, id: formId, depth: 0, overrideAccess: true, req })\n\t\t\t\t.catch(() => null)\n\n\t\t\tawait dispatchActions({\n\t\t\t\tactions: (form?.actions ?? null) as ActionInstance[] | null,\n\t\t\t\tformId,\n\t\t\t\tsubmissionId: doc.id as number | string,\n\t\t\t\tregistry: args.actionRegistry,\n\t\t\t\tpayload,\n\t\t\t\treq,\n\t\t\t\thasRunner: args.hasRunner,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait resolveEventSink(args.events).emit({\n\t\t\t\t\ttype: 'submission.created',\n\t\t\t\t\tformId: String(formId),\n\t\t\t\t\tsubmissionId: String(doc.id),\n\t\t\t\t\tat: new Date().toISOString(),\n\t\t\t\t})\n\t\t\t} catch (error) {\n\t\t\t\tpayload.logger?.error(\n\t\t\t\t\t`@10x-media/form-builder: submission.created sink threw: ${\n\t\t\t\t\t\terror instanceof Error ? error.message : String(error)\n\t\t\t\t\t}`\n\t\t\t\t)\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tpayload.logger?.error(\n\t\t\t\t`@10x-media/form-builder: afterChange dispatch failed for submission ${String(doc.id)}: ${\n\t\t\t\t\terror instanceof Error ? error.message : String(error)\n\t\t\t\t}`\n\t\t\t)\n\t\t}\n\t\treturn doc\n\t}\n\nexport const buildSubmissionsCollection = ({\n\tregistry,\n\truleRegistry,\n\tconsentRegistry,\n\tactionRegistry = new Map(),\n\tevents,\n\thasRunner = false,\n\tuploadSlug,\n\tspam,\n
|
|
1
|
+
{"version":3,"file":"formSubmissions.js","names":[],"sources":["../../src/collections/formSubmissions.ts"],"sourcesContent":["import type { CollectionAfterChangeHook, CollectionConfig, Field } from 'payload'\nimport { dispatchActions } from '../actions/dispatch'\nimport type { ActionRegistry } from '../actions/registry'\nimport type { ActionInstance } from '../actions/runActions'\nimport type { ConsentSourceRegistry } from '../consent/registry'\nimport { resolveEventSink } from '../events/resolveEventSink'\nimport type { FormEventSink } from '../events/types'\nimport type { FieldTypeRegistry } from '../fields/registry'\nimport { isLoggedIn } from '../plugin/access'\nimport type { CollectionOverrides } from '../plugin/collectionOverrides'\nimport { buildSpamGuard } from '../spam/spamGuard'\nimport type { ResolvedSpamConfig } from '../spam/types'\nimport { validateSubmission } from '../submissions/validateSubmission'\nimport { keys } from '../translations/keys'\nimport { labelForKey } from '../translations/server'\nimport type { ValidationRuleRegistry } from '../validation/registry'\nimport { FORMS_SLUG } from './forms'\n\nexport const FORM_SUBMISSIONS_SLUG = 'form-submissions'\n\ntype BuildSubmissionsCollectionArgs = {\n\tregistry: FieldTypeRegistry\n\truleRegistry: ValidationRuleRegistry\n\tconsentRegistry: ConsentSourceRegistry\n\tactionRegistry?: ActionRegistry\n\tevents?: FormEventSink\n\t/** Whether a job runner is likely present; gates the queued vs bounded-inline dispatch path. */\n\thasRunner?: boolean\n\t/** Upload collection slug for file fields without an explicit `relationTo`. */\n\tuploadSlug?: string\n\t/** Resolved spam config; when active, prepends the spam guard before validation. `false` disables it. */\n\tspam?: ResolvedSpamConfig | false\n\t/**\n\t * When `true`, shows the raw `values`, `descriptors`, and `consent` JSON fields in the admin UI.\n\t * Default `false` — they are fully represented by the `SubmissionAnswers` UI component.\n\t */\n\tshowRawFields?: boolean\n\toverrides?: CollectionOverrides\n}\n\nconst formIdOf = (form: unknown): number | string | undefined => {\n\tif (typeof form === 'number' || typeof form === 'string') {\n\t\treturn form\n\t}\n\tif (form && typeof form === 'object' && 'id' in form) {\n\t\tconst id = (form as { id: unknown }).id\n\t\tif (typeof id === 'number' || typeof id === 'string') {\n\t\t\treturn id\n\t\t}\n\t}\n\treturn undefined\n}\n\n/**\n * On a completed submission create, dispatch the form's post-submit actions and emit `submission.created`.\n * Both are side effects on an already-written row, so the whole body is wrapped: nothing it does can throw\n * past the hook (a failed action or sink must never fail the submission write). Dispatch is itself bounded\n * and non-throwing; the inline fallback is awaited so the bound caps the added latency, while the queued\n * path returns immediately. The form's `actions` are null-guarded for legacy rows created before the field\n * existed.\n */\nconst makeAfterChange =\n\t(args: {\n\t\tactionRegistry: ActionRegistry\n\t\tevents?: FormEventSink\n\t\thasRunner: boolean\n\t}): CollectionAfterChangeHook =>\n\tasync ({ doc, operation, req }) => {\n\t\tif (operation !== 'create' || (doc.status != null && doc.status !== 'complete')) {\n\t\t\treturn doc\n\t\t}\n\t\tconst { payload } = req\n\t\ttry {\n\t\t\tconst formId = formIdOf(doc.form)\n\t\t\tif (formId == null) {\n\t\t\t\treturn doc\n\t\t\t}\n\t\t\tconst form = await payload\n\t\t\t\t.findByID({ collection: FORMS_SLUG, id: formId, depth: 0, overrideAccess: true, req })\n\t\t\t\t.catch(() => null)\n\n\t\t\tawait dispatchActions({\n\t\t\t\tactions: (form?.actions ?? null) as ActionInstance[] | null,\n\t\t\t\tformId,\n\t\t\t\tsubmissionId: doc.id as number | string,\n\t\t\t\tregistry: args.actionRegistry,\n\t\t\t\tpayload,\n\t\t\t\treq,\n\t\t\t\thasRunner: args.hasRunner,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait resolveEventSink(args.events).emit({\n\t\t\t\t\ttype: 'submission.created',\n\t\t\t\t\tformId: String(formId),\n\t\t\t\t\tsubmissionId: String(doc.id),\n\t\t\t\t\tat: new Date().toISOString(),\n\t\t\t\t})\n\t\t\t} catch (error) {\n\t\t\t\tpayload.logger?.error(\n\t\t\t\t\t`@10x-media/form-builder: submission.created sink threw: ${\n\t\t\t\t\t\terror instanceof Error ? error.message : String(error)\n\t\t\t\t\t}`\n\t\t\t\t)\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tpayload.logger?.error(\n\t\t\t\t`@10x-media/form-builder: afterChange dispatch failed for submission ${String(doc.id)}: ${\n\t\t\t\t\terror instanceof Error ? error.message : String(error)\n\t\t\t\t}`\n\t\t\t)\n\t\t}\n\t\treturn doc\n\t}\n\nexport const buildSubmissionsCollection = ({\n\tregistry,\n\truleRegistry,\n\tconsentRegistry,\n\tactionRegistry = new Map(),\n\tevents,\n\thasRunner = false,\n\tuploadSlug,\n\tspam,\n\tshowRawFields = false,\n\toverrides,\n}: BuildSubmissionsCollectionArgs): CollectionConfig => {\n\tconst defaultFields: Field[] = [\n\t\t{ name: 'form', type: 'relationship', relationTo: FORMS_SLUG, required: true },\n\t\t{\n\t\t\tname: 'status',\n\t\t\ttype: 'select',\n\t\t\tdefaultValue: 'complete',\n\t\t\toptions: [\n\t\t\t\t{ label: labelForKey(keys.statusComplete), value: 'complete' },\n\t\t\t\t{ label: labelForKey(keys.statusPartial), value: 'partial' },\n\t\t\t],\n\t\t\t// Defense-in-depth at the REST layer: anonymous clients cannot set status via the API.\n\t\t\t// The validateSubmission hook also forces 'complete' server-side, so this covers both paths.\n\t\t\taccess: { create: isLoggedIn, update: isLoggedIn },\n\t\t},\n\t\t// answers UI appears first so it is the dominant view when opening a submission document.\n\t\t{\n\t\t\tname: 'answers',\n\t\t\ttype: 'ui',\n\t\t\tadmin: {\n\t\t\t\tcomponents: { Field: '@10x-media/form-builder/rsc#SubmissionAnswers' },\n\t\t\t},\n\t\t},\n\t\t{ name: 'locale', type: 'text' },\n\t\t{ name: 'values', type: 'json', admin: { hidden: !showRawFields } },\n\t\t{ name: 'descriptors', type: 'json', admin: { hidden: !showRawFields } },\n\t\t{ name: 'consent', type: 'json', admin: { hidden: !showRawFields } },\n\t\t{ name: 'meta', type: 'json' },\n\t]\n\n\treturn {\n\t\t...(overrides ?? {}),\n\t\tslug: FORM_SUBMISSIONS_SLUG,\n\t\tlabels: {\n\t\t\tsingular: labelForKey(keys.collectionSubmissionSingular),\n\t\t\tplural: labelForKey(keys.collectionSubmissionPlural),\n\t\t\t...(overrides?.labels ?? {}),\n\t\t},\n\t\tadmin: { group: 'Forms', ...(overrides?.admin ?? {}) },\n\t\taccess: {\n\t\t\tcreate: () => true,\n\t\t\tread: isLoggedIn,\n\t\t\tupdate: () => false,\n\t\t\t...(overrides?.access ?? {}),\n\t\t},\n\t\thooks: {\n\t\t\t...(overrides?.hooks ?? {}),\n\t\t\t// Spam guard + validateSubmission must remain first: they enforce the security invariant\n\t\t\t// that anonymous callers cannot bypass post-submit actions or supply a forged status.\n\t\t\t// Consumer beforeValidate hooks are appended after so they run on already-validated data.\n\t\t\tbeforeValidate: [\n\t\t\t\t...(spam ? [buildSpamGuard(spam)] : []),\n\t\t\t\tvalidateSubmission({ registry, ruleRegistry, consentRegistry, uploadSlug }),\n\t\t\t\t...(overrides?.hooks?.beforeValidate ?? []),\n\t\t\t],\n\t\t\tafterChange: [\n\t\t\t\tmakeAfterChange({ actionRegistry, events, hasRunner }),\n\t\t\t\t...(overrides?.hooks?.afterChange ?? []),\n\t\t\t],\n\t\t},\n\t\tfields: overrides?.fields ? overrides.fields({ defaultFields }) : defaultFields,\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -36,7 +36,7 @@ const providedFlowStepCount = (raw) => {
|
|
|
36
36
|
const steps = raw.steps;
|
|
37
37
|
return Array.isArray(steps) ? steps.length : 0;
|
|
38
38
|
};
|
|
39
|
-
const buildFormsCollection = ({ registry, ruleRegistry, consentRegistry, presentationRegistry = new Map(Object.entries(defaultPresentationDescriptors)), actionRegistry = /* @__PURE__ */ new Map() }) => {
|
|
39
|
+
const buildFormsCollection = ({ overrides, registry, ruleRegistry, consentRegistry, presentationRegistry = new Map(Object.entries(defaultPresentationDescriptors)), actionRegistry = /* @__PURE__ */ new Map() }) => {
|
|
40
40
|
const conditionTypes = buildConditionTypeMap(registry);
|
|
41
41
|
const FLOW_BUILDER_REF = "@10x-media/form-builder/client#FlowBuilder";
|
|
42
42
|
const beforeValidate = ({ data, req }) => {
|
|
@@ -58,127 +58,138 @@ const buildFormsCollection = ({ registry, ruleRegistry, consentRegistry, present
|
|
|
58
58
|
}
|
|
59
59
|
return data;
|
|
60
60
|
};
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
const defaultFields = [
|
|
62
|
+
{
|
|
63
|
+
name: "title",
|
|
64
|
+
type: "text",
|
|
65
|
+
required: true,
|
|
66
|
+
label: labelForKey(keys.fieldTitle)
|
|
66
67
|
},
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
{
|
|
69
|
+
name: "fields",
|
|
70
|
+
type: "blocks",
|
|
71
|
+
blocks: buildFieldBlocks(registry, ruleRegistry, consentRegistry)
|
|
70
72
|
},
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
items: { type: "string" }
|
|
109
|
-
},
|
|
110
|
-
next: { type: "string" },
|
|
111
|
-
transitions: {
|
|
112
|
-
type: "array",
|
|
113
|
-
items: {
|
|
114
|
-
type: "object",
|
|
115
|
-
required: ["to"],
|
|
116
|
-
additionalProperties: true,
|
|
117
|
-
properties: {
|
|
118
|
-
to: { type: "string" },
|
|
119
|
-
when: {
|
|
120
|
-
type: "object",
|
|
121
|
-
additionalProperties: true
|
|
122
|
-
}
|
|
73
|
+
{
|
|
74
|
+
name: "flow",
|
|
75
|
+
type: "json",
|
|
76
|
+
validate: validateFlow,
|
|
77
|
+
admin: { components: { Field: {
|
|
78
|
+
path: FLOW_BUILDER_REF,
|
|
79
|
+
clientProps: { conditionTypes }
|
|
80
|
+
} } },
|
|
81
|
+
typescriptSchema: [() => ({
|
|
82
|
+
type: "object",
|
|
83
|
+
required: ["steps"],
|
|
84
|
+
additionalProperties: false,
|
|
85
|
+
properties: { steps: {
|
|
86
|
+
type: "array",
|
|
87
|
+
items: {
|
|
88
|
+
type: "object",
|
|
89
|
+
required: ["id"],
|
|
90
|
+
additionalProperties: true,
|
|
91
|
+
properties: {
|
|
92
|
+
id: { type: "string" },
|
|
93
|
+
title: { type: "string" },
|
|
94
|
+
fields: {
|
|
95
|
+
type: "array",
|
|
96
|
+
items: { type: "string" }
|
|
97
|
+
},
|
|
98
|
+
next: { type: "string" },
|
|
99
|
+
transitions: {
|
|
100
|
+
type: "array",
|
|
101
|
+
items: {
|
|
102
|
+
type: "object",
|
|
103
|
+
required: ["to"],
|
|
104
|
+
additionalProperties: true,
|
|
105
|
+
properties: {
|
|
106
|
+
to: { type: "string" },
|
|
107
|
+
when: {
|
|
108
|
+
type: "object",
|
|
109
|
+
additionalProperties: true
|
|
123
110
|
}
|
|
124
111
|
}
|
|
125
112
|
}
|
|
126
113
|
}
|
|
127
114
|
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
],
|
|
167
|
-
endpoints: [{
|
|
168
|
-
path: "/:id/results",
|
|
169
|
-
method: "get",
|
|
170
|
-
handler: async (req) => {
|
|
171
|
-
const field = typeof req.query?.field === "string" ? req.query.field : void 0;
|
|
172
|
-
const { status, body } = await resolveFormResultsRequest({
|
|
173
|
-
payload: req.payload,
|
|
174
|
-
formId: req.routeParams?.id,
|
|
175
|
-
field,
|
|
176
|
-
isAuthed: Boolean(req.user),
|
|
177
|
-
req
|
|
178
|
-
});
|
|
179
|
-
return Response.json(body, { status });
|
|
115
|
+
}
|
|
116
|
+
} }
|
|
117
|
+
})]
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "actions",
|
|
121
|
+
type: "blocks",
|
|
122
|
+
blocks: buildActionBlocks(actionRegistry),
|
|
123
|
+
label: labelForKey(keys.configActions),
|
|
124
|
+
access: { read: isLoggedIn }
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "defaultPresentation",
|
|
128
|
+
type: "select",
|
|
129
|
+
defaultValue: DEFAULT_PRESENTATION_NAME,
|
|
130
|
+
options: [...presentationRegistry.values()].map((descriptor) => ({
|
|
131
|
+
label: labelFor(descriptor.label),
|
|
132
|
+
value: descriptor.name
|
|
133
|
+
})),
|
|
134
|
+
label: labelForKey(keys.configDefaultPresentation),
|
|
135
|
+
admin: { position: "sidebar" }
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "showResults",
|
|
139
|
+
type: "checkbox",
|
|
140
|
+
defaultValue: false,
|
|
141
|
+
label: labelForKey(keys.configShowResults),
|
|
142
|
+
admin: { position: "sidebar" }
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "resultsField",
|
|
146
|
+
type: "text",
|
|
147
|
+
label: labelForKey(keys.configResultsField),
|
|
148
|
+
admin: {
|
|
149
|
+
position: "sidebar",
|
|
150
|
+
description: "Field whose aggregate results are public when \"Show results publicly\" is on. Use a choice field, never a free-text or PII field.",
|
|
151
|
+
condition: (data) => Boolean(data?.showResults)
|
|
180
152
|
}
|
|
181
|
-
}
|
|
153
|
+
}
|
|
154
|
+
];
|
|
155
|
+
const defaultEndpoints = [{
|
|
156
|
+
path: "/:id/results",
|
|
157
|
+
method: "get",
|
|
158
|
+
handler: async (req) => {
|
|
159
|
+
const field = typeof req.query?.field === "string" ? req.query.field : void 0;
|
|
160
|
+
const { status, body } = await resolveFormResultsRequest({
|
|
161
|
+
payload: req.payload,
|
|
162
|
+
formId: req.routeParams?.id,
|
|
163
|
+
field,
|
|
164
|
+
isAuthed: Boolean(req.user),
|
|
165
|
+
req
|
|
166
|
+
});
|
|
167
|
+
return Response.json(body, { status });
|
|
168
|
+
}
|
|
169
|
+
}];
|
|
170
|
+
return {
|
|
171
|
+
...overrides ?? {},
|
|
172
|
+
slug: FORMS_SLUG,
|
|
173
|
+
labels: {
|
|
174
|
+
singular: labelForKey(keys.collectionFormSingular),
|
|
175
|
+
plural: labelForKey(keys.collectionFormPlural),
|
|
176
|
+
...overrides?.labels ?? {}
|
|
177
|
+
},
|
|
178
|
+
admin: {
|
|
179
|
+
group: "Forms",
|
|
180
|
+
useAsTitle: "title",
|
|
181
|
+
...overrides?.admin ?? {}
|
|
182
|
+
},
|
|
183
|
+
access: {
|
|
184
|
+
read: () => true,
|
|
185
|
+
...overrides?.access ?? {}
|
|
186
|
+
},
|
|
187
|
+
hooks: {
|
|
188
|
+
...overrides?.hooks ?? {},
|
|
189
|
+
beforeValidate: [beforeValidate, ...overrides?.hooks?.beforeValidate ?? []]
|
|
190
|
+
},
|
|
191
|
+
endpoints: [...defaultEndpoints, ...Array.isArray(overrides?.endpoints) ? overrides.endpoints : []],
|
|
192
|
+
fields: overrides?.fields ? overrides.fields({ defaultFields }) : defaultFields
|
|
182
193
|
};
|
|
183
194
|
};
|
|
184
195
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forms.js","names":[],"sources":["../../src/collections/forms.ts"],"sourcesContent":["import {\n\ttype CollectionBeforeValidateHook,\n\ttype CollectionConfig,\n\ttype PayloadRequest,\n\tValidationError,\n} from 'payload'\nimport { buildActionBlocks } from '../actions/buildActionBlocks'\nimport type { ActionRegistry } from '../actions/registry'\nimport { resolveFormResultsRequest } from '../aggregation/resolveResultsRequest'\nimport { normalizeCalc } from '../calc/normalizeCalc'\nimport { buildConditionTypeMap } from '../conditions/conditionType'\nimport { type FieldRow, normalizeFormConditions } from '../conditions/normalizeConditions'\nimport type { ConsentSourceRegistry } from '../consent/registry'\nimport { buildFieldBlocks } from '../fields/buildFieldBlocks'\nimport type { FieldTypeRegistry } from '../fields/registry'\nimport { normalizeFlow } from '../flow/normalizeFlow'\nimport { isLoggedIn } from '../plugin/access'\nimport {\n\tDEFAULT_PRESENTATION_NAME,\n\tdefaultPresentationDescriptors,\n} from '../presentations/defaults'\nimport type { PresentationDescriptorRegistry } from '../presentations/registry'\nimport { keys } from '../translations/keys'\nimport { labelFor, labelForKey } from '../translations/server'\nimport type { ValidationRuleRegistry } from '../validation/registry'\n\nexport const FORMS_SLUG = 'forms'\n\nconst validateFlow = (raw: unknown): string | true => {\n\tif (raw === null || raw === undefined) return true\n\tconst r = raw as Record<string, unknown>\n\tif (!Array.isArray(r.steps)) return true\n\tconst steps = r.steps as Array<Record<string, unknown>>\n\tconst emptyIdStep = steps.find((s) => typeof s?.id !== 'string' || s.id.length === 0)\n\tif (emptyIdStep) return 'Flow: every step must have a non-empty ID'\n\tconst ids = steps.map((s) => s.id as string)\n\tif (new Set(ids).size !== ids.length) {\n\t\treturn 'Flow: duplicate step IDs found'\n\t}\n\tconst idSet = new Set(ids)\n\tfor (const step of steps) {\n\t\tconst id = step.id as string\n\t\tif (typeof step.next === 'string' && step.next.length > 0 && !idSet.has(step.next)) {\n\t\t\treturn `Flow: step \"${id}\" references unknown next step \"${step.next}\"`\n\t\t}\n\t\tif (Array.isArray(step.transitions)) {\n\t\t\tfor (const t of step.transitions as Array<Record<string, unknown>>) {\n\t\t\t\tif (typeof t?.to === 'string' && t.to.length > 0 && !idSet.has(t.to)) {\n\t\t\t\t\treturn `Flow: step \"${id}\" has a transition to unknown step \"${t.to}\"`\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn true\n}\n\n/** How many steps the caller actually submitted, before normalization strips/collapses the flow. */\nconst providedFlowStepCount = (raw: unknown): number => {\n\tif (raw === null || typeof raw !== 'object') return 0\n\tconst steps = (raw as { steps?: unknown }).steps\n\treturn Array.isArray(steps) ? steps.length : 0\n}\n\ntype BuildFormsCollectionArgs = {\n\tregistry: FieldTypeRegistry\n\truleRegistry: ValidationRuleRegistry\n\tconsentRegistry?: ConsentSourceRegistry\n\tpresentationRegistry?: PresentationDescriptorRegistry\n\tactionRegistry?: ActionRegistry\n}\n\nexport const buildFormsCollection = ({\n\tregistry,\n\truleRegistry,\n\tconsentRegistry,\n\tpresentationRegistry = new Map(Object.entries(defaultPresentationDescriptors)),\n\tactionRegistry = new Map(),\n}: BuildFormsCollectionArgs): CollectionConfig => {\n\tconst conditionTypes = buildConditionTypeMap(registry)\n\tconst FLOW_BUILDER_REF = '@10x-media/form-builder/client#FlowBuilder'\n\n\tconst beforeValidate: CollectionBeforeValidateHook = ({ data, req }) => {\n\t\tif (\n\t\t\tdata &&\n\t\t\ttypeof data.defaultPresentation === 'string' &&\n\t\t\t!presentationRegistry.has(data.defaultPresentation)\n\t\t) {\n\t\t\tdata.defaultPresentation = DEFAULT_PRESENTATION_NAME\n\t\t}\n\t\tif (data && Array.isArray(data.fields)) {\n\t\t\tconst normalized: FieldRow[] = normalizeFormConditions(\n\t\t\t\tdata.fields as FieldRow[],\n\t\t\t\tconditionTypes\n\t\t\t)\n\t\t\tfor (const field of normalized) {\n\t\t\t\tif ('expression' in field) {\n\t\t\t\t\tfield.expression = normalizeCalc(field.expression)\n\t\t\t\t}\n\t\t\t}\n\t\t\tdata.fields = normalized\n\t\t\tconst fieldNames = normalized\n\t\t\t\t.map((field: FieldRow) => (typeof field.name === 'string' ? field.name : undefined))\n\t\t\t\t.filter((name): name is string => name !== undefined)\n\t\t\tconst normalizedFlow = normalizeFlow(data.flow, fieldNames)\n\t\t\t// A flow the author built but that collapses to fewer than two valid steps would\n\t\t\t// otherwise vanish silently. Surface it instead of discarding their work.\n\t\t\tif (providedFlowStepCount(data.flow) > 0 && normalizedFlow === undefined) {\n\t\t\t\tthrow new ValidationError(\n\t\t\t\t\t{\n\t\t\t\t\t\tcollection: FORMS_SLUG,\n\t\t\t\t\t\terrors: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tpath: 'flow',\n\t\t\t\t\t\t\t\tmessage:\n\t\t\t\t\t\t\t\t\t'A flow needs at least two steps with unique, non-empty IDs. Add another step or remove the flow.',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t},\n\t\t\t\t\treq.t\n\t\t\t\t)\n\t\t\t}\n\t\t\tdata.flow = normalizedFlow\n\t\t}\n\t\treturn data\n\t}\n\n\treturn {\n\t\tslug: FORMS_SLUG,\n\t\tlabels: { singular: 'Form', plural: 'Forms' },\n\t\tadmin: { group: 'Forms', useAsTitle: 'title' },\n\t\taccess: { read: () => true },\n\t\thooks: {\n\t\t\tbeforeValidate: [beforeValidate],\n\t\t},\n\t\tfields: [\n\t\t\t{ name: 'title', type: 'text', required: true, label: labelForKey(keys.fieldTitle) },\n\t\t\t{\n\t\t\t\tname: 'fields',\n\t\t\t\ttype: 'blocks',\n\t\t\t\tblocks: buildFieldBlocks(registry, ruleRegistry, consentRegistry),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'flow',\n\t\t\t\ttype: 'json',\n\t\t\t\tvalidate: validateFlow,\n\t\t\t\tadmin: {\n\t\t\t\t\tcomponents: {\n\t\t\t\t\t\tField: { path: FLOW_BUILDER_REF, clientProps: { conditionTypes } },\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t// Narrows the generated TypeScript type from opaque JSON to FormFlow so callers\n\t\t\t\t// don't need a cast. Keep this in sync with src/flow/types.ts.\n\t\t\t\ttypescriptSchema: [\n\t\t\t\t\t() => ({\n\t\t\t\t\t\ttype: 'object' as const,\n\t\t\t\t\t\trequired: ['steps'],\n\t\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\tsteps: {\n\t\t\t\t\t\t\t\ttype: 'array' as const,\n\t\t\t\t\t\t\t\titems: {\n\t\t\t\t\t\t\t\t\ttype: 'object' as const,\n\t\t\t\t\t\t\t\t\trequired: ['id'],\n\t\t\t\t\t\t\t\t\tadditionalProperties: true,\n\t\t\t\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\t\t\t\tid: { type: 'string' as const },\n\t\t\t\t\t\t\t\t\t\ttitle: { type: 'string' as const },\n\t\t\t\t\t\t\t\t\t\tfields: { type: 'array' as const, items: { type: 'string' as const } },\n\t\t\t\t\t\t\t\t\t\tnext: { type: 'string' as const },\n\t\t\t\t\t\t\t\t\t\ttransitions: {\n\t\t\t\t\t\t\t\t\t\t\ttype: 'array' as const,\n\t\t\t\t\t\t\t\t\t\t\titems: {\n\t\t\t\t\t\t\t\t\t\t\t\ttype: 'object' as const,\n\t\t\t\t\t\t\t\t\t\t\t\trequired: ['to'],\n\t\t\t\t\t\t\t\t\t\t\t\tadditionalProperties: true,\n\t\t\t\t\t\t\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\t\t\t\t\t\t\tto: { type: 'string' as const },\n\t\t\t\t\t\t\t\t\t\t\t\t\twhen: { type: 'object' as const, additionalProperties: true },\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t}),\n\t\t\t\t],\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'actions',\n\t\t\t\ttype: 'blocks',\n\t\t\t\tblocks: buildActionBlocks(actionRegistry),\n\t\t\t\tlabel: labelForKey(keys.configActions),\n\t\t\t\t// Action config can contain secrets (e.g. signedWebhook.secret). The collection\n\t\t\t\t// itself is publicly readable so forms can be rendered by anonymous clients, but\n\t\t\t\t// action config must never be exposed to anonymous callers.\n\t\t\t\taccess: { read: isLoggedIn },\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'defaultPresentation',\n\t\t\t\ttype: 'select',\n\t\t\t\tdefaultValue: DEFAULT_PRESENTATION_NAME,\n\t\t\t\toptions: [...presentationRegistry.values()].map((descriptor) => ({\n\t\t\t\t\tlabel: labelFor(descriptor.label),\n\t\t\t\t\tvalue: descriptor.name,\n\t\t\t\t})),\n\t\t\t\tlabel: labelForKey(keys.configDefaultPresentation),\n\t\t\t\tadmin: { position: 'sidebar' },\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'showResults',\n\t\t\t\ttype: 'checkbox',\n\t\t\t\tdefaultValue: false,\n\t\t\t\tlabel: labelForKey(keys.configShowResults),\n\t\t\t\tadmin: { position: 'sidebar' },\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'resultsField',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelForKey(keys.configResultsField),\n\t\t\t\tadmin: {\n\t\t\t\t\tposition: 'sidebar',\n\t\t\t\t\tdescription:\n\t\t\t\t\t\t'Field whose aggregate results are public when \"Show results publicly\" is on. Use a choice field, never a free-text or PII field.',\n\t\t\t\t\tcondition: (data) => Boolean(data?.showResults),\n\t\t\t\t},\n\t\t\t},\n\t\t],\n\t\tendpoints: [\n\t\t\t{\n\t\t\t\tpath: '/:id/results',\n\t\t\t\tmethod: 'get',\n\t\t\t\thandler: async (req: PayloadRequest) => {\n\t\t\t\t\tconst field = typeof req.query?.field === 'string' ? req.query.field : undefined\n\t\t\t\t\tconst { status, body } = await resolveFormResultsRequest({\n\t\t\t\t\t\tpayload: req.payload,\n\t\t\t\t\t\tformId: req.routeParams?.id as number | string | undefined,\n\t\t\t\t\t\tfield,\n\t\t\t\t\t\tisAuthed: Boolean(req.user),\n\t\t\t\t\t\treq,\n\t\t\t\t\t})\n\t\t\t\t\treturn Response.json(body, { status })\n\t\t\t\t},\n\t\t\t},\n\t\t],\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"forms.js","names":[],"sources":["../../src/collections/forms.ts"],"sourcesContent":["import {\n\ttype CollectionBeforeValidateHook,\n\ttype CollectionConfig,\n\ttype Field,\n\ttype PayloadRequest,\n\tValidationError,\n} from 'payload'\nimport { buildActionBlocks } from '../actions/buildActionBlocks'\nimport type { ActionRegistry } from '../actions/registry'\nimport { resolveFormResultsRequest } from '../aggregation/resolveResultsRequest'\nimport { normalizeCalc } from '../calc/normalizeCalc'\nimport { buildConditionTypeMap } from '../conditions/conditionType'\nimport { type FieldRow, normalizeFormConditions } from '../conditions/normalizeConditions'\nimport type { ConsentSourceRegistry } from '../consent/registry'\nimport { buildFieldBlocks } from '../fields/buildFieldBlocks'\nimport type { FieldTypeRegistry } from '../fields/registry'\nimport { normalizeFlow } from '../flow/normalizeFlow'\nimport { isLoggedIn } from '../plugin/access'\nimport type { CollectionOverrides } from '../plugin/collectionOverrides'\nimport {\n\tDEFAULT_PRESENTATION_NAME,\n\tdefaultPresentationDescriptors,\n} from '../presentations/defaults'\nimport type { PresentationDescriptorRegistry } from '../presentations/registry'\nimport { keys } from '../translations/keys'\nimport { labelFor, labelForKey } from '../translations/server'\nimport type { ValidationRuleRegistry } from '../validation/registry'\n\nexport const FORMS_SLUG = 'forms'\n\nconst validateFlow = (raw: unknown): string | true => {\n\tif (raw === null || raw === undefined) return true\n\tconst r = raw as Record<string, unknown>\n\tif (!Array.isArray(r.steps)) return true\n\tconst steps = r.steps as Array<Record<string, unknown>>\n\tconst emptyIdStep = steps.find((s) => typeof s?.id !== 'string' || s.id.length === 0)\n\tif (emptyIdStep) return 'Flow: every step must have a non-empty ID'\n\tconst ids = steps.map((s) => s.id as string)\n\tif (new Set(ids).size !== ids.length) {\n\t\treturn 'Flow: duplicate step IDs found'\n\t}\n\tconst idSet = new Set(ids)\n\tfor (const step of steps) {\n\t\tconst id = step.id as string\n\t\tif (typeof step.next === 'string' && step.next.length > 0 && !idSet.has(step.next)) {\n\t\t\treturn `Flow: step \"${id}\" references unknown next step \"${step.next}\"`\n\t\t}\n\t\tif (Array.isArray(step.transitions)) {\n\t\t\tfor (const t of step.transitions as Array<Record<string, unknown>>) {\n\t\t\t\tif (typeof t?.to === 'string' && t.to.length > 0 && !idSet.has(t.to)) {\n\t\t\t\t\treturn `Flow: step \"${id}\" has a transition to unknown step \"${t.to}\"`\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn true\n}\n\n/** How many steps the caller actually submitted, before normalization strips/collapses the flow. */\nconst providedFlowStepCount = (raw: unknown): number => {\n\tif (raw === null || typeof raw !== 'object') return 0\n\tconst steps = (raw as { steps?: unknown }).steps\n\treturn Array.isArray(steps) ? steps.length : 0\n}\n\ntype BuildFormsCollectionArgs = {\n\tregistry: FieldTypeRegistry\n\truleRegistry: ValidationRuleRegistry\n\tconsentRegistry?: ConsentSourceRegistry\n\tpresentationRegistry?: PresentationDescriptorRegistry\n\tactionRegistry?: ActionRegistry\n\toverrides?: CollectionOverrides\n}\n\nexport const buildFormsCollection = ({\n\toverrides,\n\tregistry,\n\truleRegistry,\n\tconsentRegistry,\n\tpresentationRegistry = new Map(Object.entries(defaultPresentationDescriptors)),\n\tactionRegistry = new Map(),\n}: BuildFormsCollectionArgs): CollectionConfig => {\n\tconst conditionTypes = buildConditionTypeMap(registry)\n\tconst FLOW_BUILDER_REF = '@10x-media/form-builder/client#FlowBuilder'\n\n\tconst beforeValidate: CollectionBeforeValidateHook = ({ data, req }) => {\n\t\tif (\n\t\t\tdata &&\n\t\t\ttypeof data.defaultPresentation === 'string' &&\n\t\t\t!presentationRegistry.has(data.defaultPresentation)\n\t\t) {\n\t\t\tdata.defaultPresentation = DEFAULT_PRESENTATION_NAME\n\t\t}\n\t\tif (data && Array.isArray(data.fields)) {\n\t\t\tconst normalized: FieldRow[] = normalizeFormConditions(\n\t\t\t\tdata.fields as FieldRow[],\n\t\t\t\tconditionTypes\n\t\t\t)\n\t\t\tfor (const field of normalized) {\n\t\t\t\tif ('expression' in field) {\n\t\t\t\t\tfield.expression = normalizeCalc(field.expression)\n\t\t\t\t}\n\t\t\t}\n\t\t\tdata.fields = normalized\n\t\t\tconst fieldNames = normalized\n\t\t\t\t.map((field: FieldRow) => (typeof field.name === 'string' ? field.name : undefined))\n\t\t\t\t.filter((name): name is string => name !== undefined)\n\t\t\tconst normalizedFlow = normalizeFlow(data.flow, fieldNames)\n\t\t\t// A flow the author built but that collapses to fewer than two valid steps would\n\t\t\t// otherwise vanish silently. Surface it instead of discarding their work.\n\t\t\tif (providedFlowStepCount(data.flow) > 0 && normalizedFlow === undefined) {\n\t\t\t\tthrow new ValidationError(\n\t\t\t\t\t{\n\t\t\t\t\t\tcollection: FORMS_SLUG,\n\t\t\t\t\t\terrors: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tpath: 'flow',\n\t\t\t\t\t\t\t\tmessage:\n\t\t\t\t\t\t\t\t\t'A flow needs at least two steps with unique, non-empty IDs. Add another step or remove the flow.',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t},\n\t\t\t\t\treq.t\n\t\t\t\t)\n\t\t\t}\n\t\t\tdata.flow = normalizedFlow\n\t\t}\n\t\treturn data\n\t}\n\n\tconst defaultFields: Field[] = [\n\t\t{ name: 'title', type: 'text', required: true, label: labelForKey(keys.fieldTitle) },\n\t\t{\n\t\t\tname: 'fields',\n\t\t\ttype: 'blocks',\n\t\t\tblocks: buildFieldBlocks(registry, ruleRegistry, consentRegistry),\n\t\t},\n\t\t{\n\t\t\tname: 'flow',\n\t\t\ttype: 'json',\n\t\t\tvalidate: validateFlow,\n\t\t\tadmin: {\n\t\t\t\tcomponents: {\n\t\t\t\t\tField: { path: FLOW_BUILDER_REF, clientProps: { conditionTypes } },\n\t\t\t\t},\n\t\t\t},\n\t\t\t// Narrows the generated TypeScript type from opaque JSON to FormFlow so callers\n\t\t\t// don't need a cast. Keep this in sync with src/flow/types.ts.\n\t\t\ttypescriptSchema: [\n\t\t\t\t() => ({\n\t\t\t\t\ttype: 'object' as const,\n\t\t\t\t\trequired: ['steps'],\n\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\tsteps: {\n\t\t\t\t\t\t\ttype: 'array' as const,\n\t\t\t\t\t\t\titems: {\n\t\t\t\t\t\t\t\ttype: 'object' as const,\n\t\t\t\t\t\t\t\trequired: ['id'],\n\t\t\t\t\t\t\t\tadditionalProperties: true,\n\t\t\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\t\t\tid: { type: 'string' as const },\n\t\t\t\t\t\t\t\t\ttitle: { type: 'string' as const },\n\t\t\t\t\t\t\t\t\tfields: { type: 'array' as const, items: { type: 'string' as const } },\n\t\t\t\t\t\t\t\t\tnext: { type: 'string' as const },\n\t\t\t\t\t\t\t\t\ttransitions: {\n\t\t\t\t\t\t\t\t\t\ttype: 'array' as const,\n\t\t\t\t\t\t\t\t\t\titems: {\n\t\t\t\t\t\t\t\t\t\t\ttype: 'object' as const,\n\t\t\t\t\t\t\t\t\t\t\trequired: ['to'],\n\t\t\t\t\t\t\t\t\t\t\tadditionalProperties: true,\n\t\t\t\t\t\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\t\t\t\t\t\tto: { type: 'string' as const },\n\t\t\t\t\t\t\t\t\t\t\t\twhen: { type: 'object' as const, additionalProperties: true },\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t],\n\t\t},\n\t\t{\n\t\t\tname: 'actions',\n\t\t\ttype: 'blocks',\n\t\t\tblocks: buildActionBlocks(actionRegistry),\n\t\t\tlabel: labelForKey(keys.configActions),\n\t\t\t// Action config can contain secrets (e.g. signedWebhook.secret). The collection\n\t\t\t// itself is publicly readable so forms can be rendered by anonymous clients, but\n\t\t\t// action config must never be exposed to anonymous callers.\n\t\t\taccess: { read: isLoggedIn },\n\t\t},\n\t\t{\n\t\t\tname: 'defaultPresentation',\n\t\t\ttype: 'select',\n\t\t\tdefaultValue: DEFAULT_PRESENTATION_NAME,\n\t\t\toptions: [...presentationRegistry.values()].map((descriptor) => ({\n\t\t\t\tlabel: labelFor(descriptor.label),\n\t\t\t\tvalue: descriptor.name,\n\t\t\t})),\n\t\t\tlabel: labelForKey(keys.configDefaultPresentation),\n\t\t\tadmin: { position: 'sidebar' },\n\t\t},\n\t\t{\n\t\t\tname: 'showResults',\n\t\t\ttype: 'checkbox',\n\t\t\tdefaultValue: false,\n\t\t\tlabel: labelForKey(keys.configShowResults),\n\t\t\tadmin: { position: 'sidebar' },\n\t\t},\n\t\t{\n\t\t\tname: 'resultsField',\n\t\t\ttype: 'text',\n\t\t\tlabel: labelForKey(keys.configResultsField),\n\t\t\tadmin: {\n\t\t\t\tposition: 'sidebar',\n\t\t\t\tdescription:\n\t\t\t\t\t'Field whose aggregate results are public when \"Show results publicly\" is on. Use a choice field, never a free-text or PII field.',\n\t\t\t\tcondition: (data) => Boolean(data?.showResults),\n\t\t\t},\n\t\t},\n\t]\n\n\tconst defaultEndpoints: CollectionConfig['endpoints'] = [\n\t\t{\n\t\t\tpath: '/:id/results',\n\t\t\tmethod: 'get',\n\t\t\thandler: async (req: PayloadRequest) => {\n\t\t\t\tconst field = typeof req.query?.field === 'string' ? req.query.field : undefined\n\t\t\t\tconst { status, body } = await resolveFormResultsRequest({\n\t\t\t\t\tpayload: req.payload,\n\t\t\t\t\tformId: req.routeParams?.id as number | string | undefined,\n\t\t\t\t\tfield,\n\t\t\t\t\tisAuthed: Boolean(req.user),\n\t\t\t\t\treq,\n\t\t\t\t})\n\t\t\t\treturn Response.json(body, { status })\n\t\t\t},\n\t\t},\n\t]\n\n\treturn {\n\t\t...(overrides ?? {}),\n\t\tslug: FORMS_SLUG,\n\t\tlabels: {\n\t\t\tsingular: labelForKey(keys.collectionFormSingular),\n\t\t\tplural: labelForKey(keys.collectionFormPlural),\n\t\t\t...(overrides?.labels ?? {}),\n\t\t},\n\t\tadmin: { group: 'Forms', useAsTitle: 'title', ...(overrides?.admin ?? {}) },\n\t\taccess: { read: () => true, ...(overrides?.access ?? {}) },\n\t\thooks: {\n\t\t\t...(overrides?.hooks ?? {}),\n\t\t\t// beforeValidate normalizes conditions and flow; consumer hooks run after\n\t\t\tbeforeValidate: [beforeValidate, ...(overrides?.hooks?.beforeValidate ?? [])],\n\t\t},\n\t\tendpoints: [\n\t\t\t...defaultEndpoints,\n\t\t\t...(Array.isArray(overrides?.endpoints) ? overrides.endpoints : []),\n\t\t],\n\t\tfields: overrides?.fields ? overrides.fields({ defaultFields }) : defaultFields,\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CollectionOverrides } from "../plugin/collectionOverrides.js";
|
|
1
2
|
import { CollectionConfig } from "payload";
|
|
2
3
|
|
|
3
4
|
//#region src/collections/uploads.d.ts
|
|
@@ -17,7 +18,7 @@ type UploadsOption = boolean | UploadsCollectionConfig;
|
|
|
17
18
|
* `imageSizes`, so it needs no `sharp`. Projects with their own upload collection set `uploads: false` and
|
|
18
19
|
* point the file field's `relationTo` at theirs.
|
|
19
20
|
*/
|
|
20
|
-
declare const buildUploadsCollection: (config?: UploadsCollectionConfig) => CollectionConfig;
|
|
21
|
+
declare const buildUploadsCollection: (config?: UploadsCollectionConfig, overrides?: CollectionOverrides) => CollectionConfig;
|
|
21
22
|
/** Resolve the `uploads` plugin option: `false` disables, `true`/object enables the built-in collection. */
|
|
22
23
|
declare const resolveUploads: (option: UploadsOption | undefined) => {
|
|
23
24
|
enabled: boolean;
|
|
@@ -8,25 +8,34 @@ const FORM_UPLOADS_SLUG = "form-uploads";
|
|
|
8
8
|
* `imageSizes`, so it needs no `sharp`. Projects with their own upload collection set `uploads: false` and
|
|
9
9
|
* point the file field's `relationTo` at theirs.
|
|
10
10
|
*/
|
|
11
|
-
const buildUploadsCollection = (config = {}) =>
|
|
12
|
-
|
|
13
|
-
access: config.access ?? {
|
|
14
|
-
create: () => true,
|
|
15
|
-
read: isLoggedIn,
|
|
16
|
-
update: isLoggedIn,
|
|
17
|
-
delete: isLoggedIn
|
|
18
|
-
},
|
|
19
|
-
admin: { group: "Forms" },
|
|
20
|
-
upload: config.upload && config.upload !== true ? config.upload : true,
|
|
21
|
-
fields: [{
|
|
11
|
+
const buildUploadsCollection = (config = {}, overrides) => {
|
|
12
|
+
const defaultFields = [{
|
|
22
13
|
name: "owner",
|
|
23
14
|
type: "text",
|
|
24
15
|
admin: {
|
|
25
16
|
readOnly: true,
|
|
26
17
|
hidden: true
|
|
27
18
|
}
|
|
28
|
-
}, ...config.fields ?? []]
|
|
29
|
-
|
|
19
|
+
}, ...config.fields ?? []];
|
|
20
|
+
return {
|
|
21
|
+
...overrides ?? {},
|
|
22
|
+
slug: config.slug ?? "form-uploads",
|
|
23
|
+
access: {
|
|
24
|
+
create: () => true,
|
|
25
|
+
read: isLoggedIn,
|
|
26
|
+
update: isLoggedIn,
|
|
27
|
+
delete: isLoggedIn,
|
|
28
|
+
...config.access ?? {},
|
|
29
|
+
...overrides?.access ?? {}
|
|
30
|
+
},
|
|
31
|
+
admin: {
|
|
32
|
+
group: "Forms",
|
|
33
|
+
...overrides?.admin ?? {}
|
|
34
|
+
},
|
|
35
|
+
upload: config.upload && config.upload !== true ? config.upload : true,
|
|
36
|
+
fields: overrides?.fields ? overrides.fields({ defaultFields }) : defaultFields
|
|
37
|
+
};
|
|
38
|
+
};
|
|
30
39
|
/** Resolve the `uploads` plugin option: `false` disables, `true`/object enables the built-in collection. */
|
|
31
40
|
const resolveUploads = (option) => {
|
|
32
41
|
if (option === false) return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"uploads.js","names":[],"sources":["../../src/collections/uploads.ts"],"sourcesContent":["import type { CollectionConfig } from 'payload'\nimport { isLoggedIn } from '../plugin/access'\n\nexport const FORM_UPLOADS_SLUG = 'form-uploads'\n\n/** Override surface for the built-in upload collection. */\nexport type UploadsCollectionConfig = {\n\tslug?: string\n\t/** Merged onto the collection's `upload` config (e.g. `staticDir`, `mimeTypes`). `true` uses Payload defaults. */\n\tupload?: NonNullable<CollectionConfig['upload']> | true\n\t/** Replace access (defaults: anonymous create, authed read/update/delete). */\n\taccess?: CollectionConfig['access']\n\t/** Append extra fields. */\n\tfields?: CollectionConfig['fields']\n}\n\nexport type UploadsOption = boolean | UploadsCollectionConfig\n\n/**\n * The built-in upload collection backing the `file` field. Anonymous create (public forms upload here),\n * authed read/update/delete (only admins read stored files; submitters cannot enumerate others' uploads).\n * Per-field MIME/size is re-enforced at submit, so the collection stays permissive by default. No\n * `imageSizes`, so it needs no `sharp`. Projects with their own upload collection set `uploads: false` and\n * point the file field's `relationTo` at theirs.\n */\nexport const buildUploadsCollection = (
|
|
1
|
+
{"version":3,"file":"uploads.js","names":[],"sources":["../../src/collections/uploads.ts"],"sourcesContent":["import type { CollectionConfig, Field } from 'payload'\nimport { isLoggedIn } from '../plugin/access'\nimport type { CollectionOverrides } from '../plugin/collectionOverrides'\n\nexport const FORM_UPLOADS_SLUG = 'form-uploads'\n\n/** Override surface for the built-in upload collection. */\nexport type UploadsCollectionConfig = {\n\tslug?: string\n\t/** Merged onto the collection's `upload` config (e.g. `staticDir`, `mimeTypes`). `true` uses Payload defaults. */\n\tupload?: NonNullable<CollectionConfig['upload']> | true\n\t/** Replace access (defaults: anonymous create, authed read/update/delete). */\n\taccess?: CollectionConfig['access']\n\t/** Append extra fields. */\n\tfields?: CollectionConfig['fields']\n}\n\nexport type UploadsOption = boolean | UploadsCollectionConfig\n\n/**\n * The built-in upload collection backing the `file` field. Anonymous create (public forms upload here),\n * authed read/update/delete (only admins read stored files; submitters cannot enumerate others' uploads).\n * Per-field MIME/size is re-enforced at submit, so the collection stays permissive by default. No\n * `imageSizes`, so it needs no `sharp`. Projects with their own upload collection set `uploads: false` and\n * point the file field's `relationTo` at theirs.\n */\nexport const buildUploadsCollection = (\n\tconfig: UploadsCollectionConfig = {},\n\toverrides?: CollectionOverrides\n): CollectionConfig => {\n\tconst defaultFields: Field[] = [\n\t\t{ name: 'owner', type: 'text', admin: { readOnly: true, hidden: true } },\n\t\t...(config.fields ?? []),\n\t]\n\n\treturn {\n\t\t...(overrides ?? {}),\n\t\tslug: config.slug ?? FORM_UPLOADS_SLUG,\n\t\taccess: {\n\t\t\tcreate: () => true,\n\t\t\tread: isLoggedIn,\n\t\t\tupdate: isLoggedIn,\n\t\t\tdelete: isLoggedIn,\n\t\t\t...(config.access ?? {}),\n\t\t\t...(overrides?.access ?? {}),\n\t\t},\n\t\tadmin: { group: 'Forms', ...(overrides?.admin ?? {}) },\n\t\tupload: config.upload && config.upload !== true ? config.upload : true,\n\t\tfields: overrides?.fields ? overrides.fields({ defaultFields }) : defaultFields,\n\t}\n}\n\n/** Resolve the `uploads` plugin option: `false` disables, `true`/object enables the built-in collection. */\nexport const resolveUploads = (\n\toption: UploadsOption | undefined\n): { enabled: boolean; slug: string; collection?: CollectionConfig } => {\n\tif (option === false) {\n\t\treturn { enabled: false, slug: FORM_UPLOADS_SLUG }\n\t}\n\tconst config = option && option !== true ? option : {}\n\tconst collection = buildUploadsCollection(config)\n\treturn { enabled: true, slug: collection.slug as string, collection }\n}\n"],"mappings":";;AAIA,MAAa,oBAAoB;;;;;;;;AAsBjC,MAAa,0BACZ,SAAkC,CAAC,GACnC,cACsB;CACtB,MAAM,gBAAyB,CAC9B;EAAE,MAAM;EAAS,MAAM;EAAQ,OAAO;GAAE,UAAU;GAAM,QAAQ;EAAK;CAAE,GACvE,GAAI,OAAO,UAAU,CAAC,CACvB;CAEA,OAAO;EACN,GAAI,aAAa,CAAC;EAClB,MAAM,OAAO,QAAA;EACb,QAAQ;GACP,cAAc;GACd,MAAM;GACN,QAAQ;GACR,QAAQ;GACR,GAAI,OAAO,UAAU,CAAC;GACtB,GAAI,WAAW,UAAU,CAAC;EAC3B;EACA,OAAO;GAAE,OAAO;GAAS,GAAI,WAAW,SAAS,CAAC;EAAG;EACrD,QAAQ,OAAO,UAAU,OAAO,WAAW,OAAO,OAAO,SAAS;EAClE,QAAQ,WAAW,SAAS,UAAU,OAAO,EAAE,cAAc,CAAC,IAAI;CACnE;AACD;;AAGA,MAAa,kBACZ,WACuE;CACvE,IAAI,WAAW,OACd,OAAO;EAAE,SAAS;EAAO,MAAM;CAAkB;CAGlD,MAAM,aAAa,uBADJ,UAAU,WAAW,OAAO,SAAS,CAAC,CACL;CAChD,OAAO;EAAE,SAAS;EAAM,MAAM,WAAW;EAAgB;CAAW;AACrE"}
|