@10x-media/form-builder 0.1.0-beta.1 → 0.1.0-beta.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.
- package/CHANGELOG.md +34 -0
- package/README.md +4 -2
- package/dist/actions/body/converters.d.ts +20 -0
- package/dist/actions/body/converters.js +57 -0
- package/dist/actions/body/converters.js.map +1 -0
- package/dist/actions/body/escapeHtml.d.ts +6 -0
- package/dist/actions/body/escapeHtml.js +14 -0
- package/dist/actions/body/escapeHtml.js.map +1 -0
- package/dist/actions/body/serializeBody.d.ts +35 -0
- package/dist/actions/body/serializeBody.js +76 -0
- package/dist/actions/body/serializeBody.js.map +1 -0
- package/dist/actions/body/serializeSlate.js +31 -0
- package/dist/actions/body/serializeSlate.js.map +1 -0
- package/dist/actions/body/wildcards.d.ts +10 -0
- package/dist/actions/body/wildcards.js +33 -0
- package/dist/actions/body/wildcards.js.map +1 -0
- package/dist/actions/builtin/confirmation.js +28 -12
- package/dist/actions/builtin/confirmation.js.map +1 -1
- package/dist/actions/builtin/emailTeam.js +7 -9
- package/dist/actions/builtin/emailTeam.js.map +1 -1
- package/dist/actions/builtin/signedWebhook.js +21 -3
- package/dist/actions/builtin/signedWebhook.js.map +1 -1
- package/dist/actions/defineAction.d.ts +2 -1
- package/dist/actions/defineAction.js.map +1 -1
- package/dist/actions/dispatch.js +2 -1
- package/dist/actions/dispatch.js.map +1 -1
- package/dist/actions/runActions.js +8 -1
- package/dist/actions/runActions.js.map +1 -1
- package/dist/actions/task.js +7 -5
- package/dist/actions/task.js.map +1 -1
- package/dist/calc/normalizeCalc.js +5 -17
- package/dist/calc/normalizeCalc.js.map +1 -1
- package/dist/calc/types.d.ts +8 -2
- package/dist/calc/types.js +22 -0
- package/dist/calc/types.js.map +1 -0
- package/dist/client/ByteSizeField.d.ts +21 -0
- package/dist/client/ByteSizeField.js +56 -0
- package/dist/client/ByteSizeField.js.map +1 -0
- package/dist/client/ConditionBuilder.js +2 -1
- package/dist/client/ConditionBuilder.js.map +1 -1
- package/dist/client/FieldNameSelect.d.ts +20 -0
- package/dist/client/FieldNameSelect.js +58 -0
- package/dist/client/FieldNameSelect.js.map +1 -0
- package/dist/client/FlowBuilder.d.ts +0 -1
- package/dist/client/FlowBuilder.js +4 -6
- package/dist/client/FlowBuilder.js.map +1 -1
- package/dist/client/FormConditionField.js +1 -4
- package/dist/client/FormConditionField.js.map +1 -1
- package/dist/client/admin.css +192 -0
- package/dist/client/toStaticLabel.js +13 -0
- package/dist/client/toStaticLabel.js.map +1 -0
- package/dist/collections/formSubmissions.js +60 -39
- package/dist/collections/formSubmissions.js.map +1 -1
- package/dist/collections/forms.js +143 -115
- 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/client.d.ts +3 -1
- package/dist/exports/client.js +3 -1
- package/dist/exports/react.d.ts +7 -2
- package/dist/exports/react.js +6 -2
- package/dist/exports/types.d.ts +2 -1
- package/dist/fields/buildFieldBlocks.js +22 -12
- package/dist/fields/buildFieldBlocks.js.map +1 -1
- package/dist/fields/builtin/calculation.js +115 -2
- package/dist/fields/builtin/calculation.js.map +1 -1
- package/dist/fields/builtin/date.js +29 -0
- package/dist/fields/builtin/date.js.map +1 -0
- package/dist/fields/builtin/file.d.ts +15 -0
- package/dist/fields/builtin/file.js +79 -3
- package/dist/fields/builtin/file.js.map +1 -1
- package/dist/fields/builtin/index.d.ts +14 -0
- package/dist/fields/builtin/index.js +13 -2
- 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/fieldNamesOfType.js +25 -0
- package/dist/fields/fieldNamesOfType.js.map +1 -0
- package/dist/fields/sharedConfig.js +30 -18
- package/dist/fields/sharedConfig.js.map +1 -1
- package/dist/fields/types.d.ts +2 -1
- package/dist/index.d.ts +38 -2
- package/dist/index.js +17 -5
- package/dist/index.js.map +1 -1
- package/dist/plugin/collectionOverrides.d.ts +22 -0
- package/dist/plugin/registerCollections.js +39 -10
- package/dist/plugin/registerCollections.js.map +1 -1
- package/dist/react/Form.d.ts +32 -3
- package/dist/react/Form.js +50 -8
- package/dist/react/Form.js.map +1 -1
- package/dist/react/FormContext.js.map +1 -1
- package/dist/react/Poll.js +1 -1
- package/dist/react/captcha/HcaptchaCaptcha.d.ts +28 -0
- package/dist/react/captcha/HcaptchaCaptcha.js +53 -0
- package/dist/react/captcha/HcaptchaCaptcha.js.map +1 -0
- package/dist/react/captcha/RecaptchaCaptcha.d.ts +41 -0
- package/dist/react/captcha/RecaptchaCaptcha.js +93 -0
- package/dist/react/captcha/RecaptchaCaptcha.js.map +1 -0
- package/dist/react/captcha/TurnstileCaptcha.d.ts +28 -0
- package/dist/react/captcha/TurnstileCaptcha.js +53 -0
- package/dist/react/captcha/TurnstileCaptcha.js.map +1 -0
- package/dist/react/captcha/types.d.ts +8 -0
- package/dist/react/captcha/useCaptchaScript.js +45 -0
- package/dist/react/captcha/useCaptchaScript.js.map +1 -0
- package/dist/react/captcha/vendors.js +8 -0
- package/dist/react/captcha/vendors.js.map +1 -0
- package/dist/react/primitives/Input.d.ts +1 -1
- package/dist/react/primitives/Input.js.map +1 -1
- package/dist/react/renderers/calculation.js +1 -1
- package/dist/react/renderers/checkbox.js +1 -1
- package/dist/react/renderers/consent.js +1 -1
- package/dist/react/renderers/date.js +37 -0
- package/dist/react/renderers/date.js.map +1 -0
- package/dist/react/renderers/email.js +1 -1
- package/dist/react/renderers/file.js +20 -7
- package/dist/react/renderers/file.js.map +1 -1
- package/dist/react/renderers/index.js +4 -0
- package/dist/react/renderers/index.js.map +1 -1
- package/dist/react/renderers/number.js +1 -1
- package/dist/react/renderers/repeater.js +113 -0
- package/dist/react/renderers/repeater.js.map +1 -0
- package/dist/react/renderers/select.js +1 -1
- package/dist/react/renderers/text.js +1 -1
- package/dist/react/renderers/textarea.js +1 -1
- package/dist/spam/captcha.d.ts +3 -2
- package/dist/spam/captcha.js +3 -2
- package/dist/spam/captcha.js.map +1 -1
- package/dist/spam/clientIp.js +9 -0
- package/dist/spam/clientIp.js.map +1 -0
- package/dist/spam/identify.js +3 -6
- package/dist/spam/identify.js.map +1 -1
- package/dist/spam/providers/hcaptcha.d.ts +17 -0
- package/dist/spam/providers/hcaptcha.js +18 -0
- package/dist/spam/providers/hcaptcha.js.map +1 -0
- package/dist/spam/providers/recaptcha.d.ts +23 -0
- package/dist/spam/providers/recaptcha.js +23 -0
- package/dist/spam/providers/recaptcha.js.map +1 -0
- package/dist/spam/providers/siteverify.js +34 -0
- package/dist/spam/providers/siteverify.js.map +1 -0
- package/dist/spam/providers/turnstile.d.ts +17 -0
- package/dist/spam/providers/turnstile.js +18 -0
- package/dist/spam/providers/turnstile.js.map +1 -0
- package/dist/spam/spamGuard.js +2 -5
- package/dist/spam/spamGuard.js.map +1 -1
- package/dist/spam/types.d.ts +2 -2
- 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 +52 -5
- package/dist/translations/en.js.map +1 -1
- package/dist/translations/keys.d.ts +51 -4
- package/dist/translations/keys.js +52 -5
- package/dist/translations/keys.js.map +1 -1
- package/dist/uploads/formatBytes.d.ts +9 -0
- package/dist/uploads/formatBytes.js +25 -0
- package/dist/uploads/formatBytes.js.map +1 -0
- package/dist/uploads/resolveFileRef.js +1 -1
- package/dist/uploads/resolveFileRef.js.map +1 -1
- package/dist/validation/builtin/index.d.ts +14 -0
- package/dist/validation/builtin/index.js +12 -1
- package/dist/validation/builtin/index.js.map +1 -1
- package/dist/validation/builtin/maxDate.js +23 -0
- package/dist/validation/builtin/maxDate.js.map +1 -0
- package/dist/validation/builtin/minDate.js +23 -0
- package/dist/validation/builtin/minDate.js.map +1 -0
- package/dist/validation/builtin/validateDateBound.js +18 -0
- package/dist/validation/builtin/validateDateBound.js.map +1 -0
- package/dist/validation/runValidation.js +1 -1
- package/dist/validation/runValidation.js.map +1 -1
- package/package.json +4 -3
- package/styles/form-builder.css +0 -190
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { CaptchaProvider } from "../types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/spam/providers/turnstile.d.ts
|
|
4
|
+
type TurnstileProviderOptions = {
|
|
5
|
+
/** Turnstile secret key (server-side, never the site key). */secretKey: string; /** Override the siteverify endpoint. */
|
|
6
|
+
verifyUrl?: string; /** Header read for the client IP forwarded as `remoteip`. Default 'x-forwarded-for'. */
|
|
7
|
+
ipHeader?: string; /** Verification request timeout in ms. Default 5000. */
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Cloudflare Turnstile verification adapter. Network errors, timeouts, and non-2xx responses
|
|
12
|
+
* fail closed (the submission is rejected).
|
|
13
|
+
*/
|
|
14
|
+
declare const turnstileProvider: (options: TurnstileProviderOptions) => CaptchaProvider;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { TurnstileProviderOptions, turnstileProvider };
|
|
17
|
+
//# sourceMappingURL=turnstile.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineCaptchaProvider } from "../captcha.js";
|
|
2
|
+
import { postSiteverify, siteverifyParams } from "./siteverify.js";
|
|
3
|
+
//#region src/spam/providers/turnstile.ts
|
|
4
|
+
const TURNSTILE_VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
|
|
5
|
+
/**
|
|
6
|
+
* Cloudflare Turnstile verification adapter. Network errors, timeouts, and non-2xx responses
|
|
7
|
+
* fail closed (the submission is rejected).
|
|
8
|
+
*/
|
|
9
|
+
const turnstileProvider = (options) => defineCaptchaProvider({
|
|
10
|
+
type: "turnstile",
|
|
11
|
+
verify: async ({ token, req }) => {
|
|
12
|
+
return (await postSiteverify(options.verifyUrl ?? TURNSTILE_VERIFY_URL, siteverifyParams(options, token, req), options.timeoutMs ?? 5e3))?.success === true;
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
//#endregion
|
|
16
|
+
export { turnstileProvider };
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=turnstile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turnstile.js","names":[],"sources":["../../../src/spam/providers/turnstile.ts"],"sourcesContent":["import { defineCaptchaProvider } from '../captcha'\nimport type { CaptchaProvider } from '../types'\nimport { DEFAULT_SITEVERIFY_TIMEOUT, postSiteverify, siteverifyParams } from './siteverify'\n\nconst TURNSTILE_VERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'\n\nexport type TurnstileProviderOptions = {\n\t/** Turnstile secret key (server-side, never the site key). */\n\tsecretKey: string\n\t/** Override the siteverify endpoint. */\n\tverifyUrl?: string\n\t/** Header read for the client IP forwarded as `remoteip`. Default 'x-forwarded-for'. */\n\tipHeader?: string\n\t/** Verification request timeout in ms. Default 5000. */\n\ttimeoutMs?: number\n}\n\n/**\n * Cloudflare Turnstile verification adapter. Network errors, timeouts, and non-2xx responses\n * fail closed (the submission is rejected).\n */\nexport const turnstileProvider = (options: TurnstileProviderOptions): CaptchaProvider =>\n\tdefineCaptchaProvider({\n\t\ttype: 'turnstile',\n\t\tverify: async ({ token, req }) => {\n\t\t\tconst result = await postSiteverify(\n\t\t\t\toptions.verifyUrl ?? TURNSTILE_VERIFY_URL,\n\t\t\t\tsiteverifyParams(options, token, req),\n\t\t\t\toptions.timeoutMs ?? DEFAULT_SITEVERIFY_TIMEOUT\n\t\t\t)\n\t\t\treturn result?.success === true\n\t\t},\n\t})\n"],"mappings":";;;AAIA,MAAM,uBAAuB;;;;;AAiB7B,MAAa,qBAAqB,YACjC,sBAAsB;CACrB,MAAM;CACN,QAAQ,OAAO,EAAE,OAAO,UAAU;EAMjC,QAAO,MALc,eACpB,QAAQ,aAAa,sBACrB,iBAAiB,SAAS,OAAO,GAAG,GACpC,QAAQ,aAAA,GACT,IACe,YAAY;CAC5B;AACD,CAAC"}
|
package/dist/spam/spamGuard.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { keys } from "../translations/keys.js";
|
|
2
2
|
import { asTranslate } from "../translations/server.js";
|
|
3
|
+
import { firstHop } from "./clientIp.js";
|
|
3
4
|
import { IDENTITY_CONTEXT_KEY } from "./constants.js";
|
|
4
5
|
import { extractReservedValues, isHoneypotTripped } from "./reserved.js";
|
|
5
6
|
import { APIError } from "payload";
|
|
6
7
|
//#region src/spam/spamGuard.ts
|
|
7
|
-
const firstHop = (req, header) => {
|
|
8
|
-
const raw = req.headers?.get(header);
|
|
9
|
-
return raw ? raw.split(",")[0]?.trim() ?? void 0 : void 0;
|
|
10
|
-
};
|
|
11
8
|
/**
|
|
12
9
|
* Submissions spam guard, prepended before `validateSubmission` in `beforeValidate` so it rejects before
|
|
13
10
|
* the form load + field validation. On create it: resolves the request identity once (stashing it on
|
|
@@ -49,7 +46,7 @@ const buildSpamGuard = (spam) => async ({ data, operation, req }) => {
|
|
|
49
46
|
spam: { captcha: captchaChecked ? "passed" : "skipped" }
|
|
50
47
|
};
|
|
51
48
|
if (spam.metadata.ip) {
|
|
52
|
-
const ip = firstHop(req, spam.ipHeader);
|
|
49
|
+
const ip = firstHop(req.headers, spam.ipHeader);
|
|
53
50
|
if (ip) serverMeta.ip = ip;
|
|
54
51
|
}
|
|
55
52
|
if (spam.metadata.ua) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spamGuard.js","names":[],"sources":["../../src/spam/spamGuard.ts"],"sourcesContent":["import { APIError, type CollectionBeforeValidateHook } from 'payload'\nimport type { SubmissionValue } from '../submissions/types'\nimport { keys } from '../translations/keys'\nimport { asTranslate } from '../translations/server'\nimport { IDENTITY_CONTEXT_KEY } from './constants'\nimport { extractReservedValues, isHoneypotTripped } from './reserved'\nimport type { ResolvedSpamConfig } from './types'\n\
|
|
1
|
+
{"version":3,"file":"spamGuard.js","names":[],"sources":["../../src/spam/spamGuard.ts"],"sourcesContent":["import { APIError, type CollectionBeforeValidateHook } from 'payload'\nimport type { SubmissionValue } from '../submissions/types'\nimport { keys } from '../translations/keys'\nimport { asTranslate } from '../translations/server'\nimport { firstHop } from './clientIp'\nimport { IDENTITY_CONTEXT_KEY } from './constants'\nimport { extractReservedValues, isHoneypotTripped } from './reserved'\nimport type { ResolvedSpamConfig } from './types'\n\n/**\n * Submissions spam guard, prepended before `validateSubmission` in `beforeValidate` so it rejects before\n * the form load + field validation. On create it: resolves the request identity once (stashing it on\n * `req.context` for upload-ownership verification downstream); rate-limits per form + identity (429,\n * fail-open when identity is null); strips reserved `values` entries (honeypot, captcha token); rejects a\n * filled honeypot with a generic error; verifies a configured captcha; and writes a server-authoritative\n * `meta` (timestamp + spam signal, with opt-in ip/ua). App-level rate limiting is defense-in-depth that\n * complements edge/CDN/WAF limiting, not a DoS replacement.\n */\nexport const buildSpamGuard =\n\t(spam: ResolvedSpamConfig): CollectionBeforeValidateHook =>\n\tasync ({ data, operation, req }) => {\n\t\tif (operation !== 'create' || !data) {\n\t\t\treturn data\n\t\t}\n\t\tconst t = asTranslate(req.i18n.t)\n\t\t// Honeypot + captcha are anti-bot measures for the anonymous public path; an authenticated request\n\t\t// (admin panel, logged-in API client) is already gated by auth and never carries a decoy/token, so\n\t\t// checking it would falsely reject legitimate authed creates. Rate-limit + ownership still apply.\n\t\tconst authenticated = Boolean(req.user)\n\n\t\tconst identity = await spam.identify(req)\n\t\tif (identity != null) {\n\t\t\treq.context[IDENTITY_CONTEXT_KEY] = identity\n\t\t}\n\n\t\tif (spam.rateLimit !== false && identity != null) {\n\t\t\tconst formKey = data.form != null ? String(data.form) : 'unknown'\n\t\t\tconst { ok } = await spam.rateLimit.limiter.check({\n\t\t\t\tkey: `submissions:${formKey}:${identity}`,\n\t\t\t\tmax: spam.rateLimit.max,\n\t\t\t\twindow: spam.rateLimit.window,\n\t\t\t\treq,\n\t\t\t})\n\t\t\tif (!ok) {\n\t\t\t\tthrow new APIError(t(keys.spamRateLimited), 429)\n\t\t\t}\n\t\t}\n\n\t\tconst honeypotField = spam.honeypot === false ? null : spam.honeypot.fieldName\n\t\tconst values = (data.values as SubmissionValue[] | undefined) ?? []\n\t\tconst { cleaned, honeypot, captchaToken } = extractReservedValues(values, honeypotField)\n\t\tdata.values = cleaned\n\n\t\tif (!authenticated && honeypotField !== null && isHoneypotTripped(honeypot)) {\n\t\t\tthrow new APIError(t(keys.spamRejected), 400)\n\t\t}\n\n\t\tconst captchaChecked = Boolean(spam.captcha) && !authenticated\n\t\tif (spam.captcha && !authenticated) {\n\t\t\tconst passed =\n\t\t\t\ttypeof captchaToken === 'string' && captchaToken.length > 0\n\t\t\t\t\t? await spam.captcha.verify({ token: captchaToken, req }).catch(() => false)\n\t\t\t\t\t: false\n\t\t\tif (!passed) {\n\t\t\t\tthrow new APIError(t(keys.spamCaptchaFailed), 400)\n\t\t\t}\n\t\t}\n\n\t\tconst serverMeta: Record<string, unknown> = {\n\t\t\tat: new Date().toISOString(),\n\t\t\tspam: { captcha: captchaChecked ? 'passed' : 'skipped' },\n\t\t}\n\t\tif (spam.metadata.ip) {\n\t\t\tconst ip = firstHop(req.headers, spam.ipHeader)\n\t\t\tif (ip) {\n\t\t\t\tserverMeta.ip = ip\n\t\t\t}\n\t\t}\n\t\tif (spam.metadata.ua) {\n\t\t\tconst ua = req.headers?.get('user-agent')\n\t\t\tif (ua) {\n\t\t\t\tserverMeta.ua = ua\n\t\t\t}\n\t\t}\n\t\t// Preserve an authenticated (trusted) caller's own `meta` keys, with server fields winning. Anonymous\n\t\t// `meta` is never trusted, so it is discarded.\n\t\tconst clientMeta =\n\t\t\tauthenticated && data.meta != null && typeof data.meta === 'object'\n\t\t\t\t? (data.meta as Record<string, unknown>)\n\t\t\t\t: {}\n\t\tdata.meta = { ...clientMeta, ...serverMeta }\n\n\t\treturn data\n\t}\n"],"mappings":";;;;;;;;;;;;;;;;AAkBA,MAAa,kBACX,SACD,OAAO,EAAE,MAAM,WAAW,UAAU;CACnC,IAAI,cAAc,YAAY,CAAC,MAC9B,OAAO;CAER,MAAM,IAAI,YAAY,IAAI,KAAK,CAAC;CAIhC,MAAM,gBAAgB,QAAQ,IAAI,IAAI;CAEtC,MAAM,WAAW,MAAM,KAAK,SAAS,GAAG;CACxC,IAAI,YAAY,MACf,IAAI,QAAQ,wBAAwB;CAGrC,IAAI,KAAK,cAAc,SAAS,YAAY,MAAM;EACjD,MAAM,UAAU,KAAK,QAAQ,OAAO,OAAO,KAAK,IAAI,IAAI;EACxD,MAAM,EAAE,OAAO,MAAM,KAAK,UAAU,QAAQ,MAAM;GACjD,KAAK,eAAe,QAAQ,GAAG;GAC/B,KAAK,KAAK,UAAU;GACpB,QAAQ,KAAK,UAAU;GACvB;EACD,CAAC;EACD,IAAI,CAAC,IACJ,MAAM,IAAI,SAAS,EAAE,KAAK,eAAe,GAAG,GAAG;CAEjD;CAEA,MAAM,gBAAgB,KAAK,aAAa,QAAQ,OAAO,KAAK,SAAS;CAErE,MAAM,EAAE,SAAS,UAAU,iBAAiB,sBAD5B,KAAK,UAA4C,CAAC,GACQ,aAAa;CACvF,KAAK,SAAS;CAEd,IAAI,CAAC,iBAAiB,kBAAkB,QAAQ,kBAAkB,QAAQ,GACzE,MAAM,IAAI,SAAS,EAAE,KAAK,YAAY,GAAG,GAAG;CAG7C,MAAM,iBAAiB,QAAQ,KAAK,OAAO,KAAK,CAAC;CACjD,IAAI,KAAK,WAAW,CAAC;MAKhB,EAHH,OAAO,iBAAiB,YAAY,aAAa,SAAS,IACvD,MAAM,KAAK,QAAQ,OAAO;GAAE,OAAO;GAAc;EAAI,CAAC,EAAE,YAAY,KAAK,IACzE,QAEH,MAAM,IAAI,SAAS,EAAE,KAAK,iBAAiB,GAAG,GAAG;CAAA;CAInD,MAAM,aAAsC;EAC3C,qBAAI,IAAI,KAAK,GAAE,YAAY;EAC3B,MAAM,EAAE,SAAS,iBAAiB,WAAW,UAAU;CACxD;CACA,IAAI,KAAK,SAAS,IAAI;EACrB,MAAM,KAAK,SAAS,IAAI,SAAS,KAAK,QAAQ;EAC9C,IAAI,IACH,WAAW,KAAK;CAElB;CACA,IAAI,KAAK,SAAS,IAAI;EACrB,MAAM,KAAK,IAAI,SAAS,IAAI,YAAY;EACxC,IAAI,IACH,WAAW,KAAK;CAElB;CAOA,KAAK,OAAO;EAAE,GAHb,iBAAiB,KAAK,QAAQ,QAAQ,OAAO,KAAK,SAAS,WACvD,KAAK,OACN,CAAC;EACwB,GAAG;CAAW;CAE3C,OAAO;AACR"}
|
package/dist/spam/types.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ type CaptchaVerifyArgs = {
|
|
|
22
22
|
token: string;
|
|
23
23
|
req: PayloadRequest;
|
|
24
24
|
};
|
|
25
|
-
/** A captcha adapter.
|
|
25
|
+
/** A captcha adapter. Use a bundled provider (Turnstile/reCAPTCHA/hCaptcha) or build one with `defineCaptchaProvider`. */
|
|
26
26
|
type CaptchaProvider = {
|
|
27
27
|
type: string;
|
|
28
28
|
verify(args: CaptchaVerifyArgs): Promise<boolean>;
|
|
@@ -41,7 +41,7 @@ type SpamConfig = {
|
|
|
41
41
|
fieldName?: string;
|
|
42
42
|
}; /** Per-identity rate limit on submission create. Default on (60s window, max 5). `false` disables. */
|
|
43
43
|
rateLimit?: false | RateLimitConfig; /** Per-identity rate limit on upload create. Default on (60s window, max 20). `false` disables. */
|
|
44
|
-
uploadRateLimit?: false | RateLimitConfig; /** A captcha provider (none by default
|
|
44
|
+
uploadRateLimit?: false | RateLimitConfig; /** A captcha provider (none by default): `turnstileProvider`/`recaptchaProvider`/`hcaptchaProvider` or a custom adapter. */
|
|
45
45
|
captcha?: CaptchaProvider; /** Identity resolution for rate-limiting, upload ownership, and (future) poll dedup. Default: user id, else trusted IP header. */
|
|
46
46
|
identify?: IdentifyFn; /** Header read for the client IP (proxy-dependent, best-effort). Default 'x-forwarded-for'. */
|
|
47
47
|
ipHeader?: string; /** Opt-in capture of client metadata onto the submission `meta`. Off by default. */
|
|
@@ -2,10 +2,8 @@ import { UIFieldServerProps } from "payload";
|
|
|
2
2
|
|
|
3
3
|
//#region src/submissions/SubmissionAnswers.d.ts
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* locale and i18n, with no client bundle. The built-in field types are formatted directly; threading
|
|
8
|
-
* the full resolved registry (custom types) to the view is a future enhancement.
|
|
5
|
+
* Server component. Formats submission data into serializable props and delegates
|
|
6
|
+
* rendering to the client component which uses Payload's UI components.
|
|
9
7
|
*/
|
|
10
8
|
declare const SubmissionAnswers: ({
|
|
11
9
|
data,
|
|
@@ -2,25 +2,23 @@ import { keys } from "../translations/keys.js";
|
|
|
2
2
|
import { asFieldTranslate } from "../translations/server.js";
|
|
3
3
|
import { defaultFieldDefinitions } from "../fields/builtin/index.js";
|
|
4
4
|
import { buildRegistry } from "../fields/registry.js";
|
|
5
|
-
import {
|
|
5
|
+
import { SubmissionAnswersClient } from "./SubmissionAnswersClient.js";
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
7
|
//#region src/submissions/SubmissionAnswers.tsx
|
|
7
8
|
const registry = buildRegistry(defaultFieldDefinitions);
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
const formatDate = (iso, locale) => {
|
|
10
|
+
try {
|
|
11
|
+
return new Intl.DateTimeFormat(locale, {
|
|
12
|
+
dateStyle: "medium",
|
|
13
|
+
timeStyle: "short"
|
|
14
|
+
}).format(new Date(iso));
|
|
15
|
+
} catch {
|
|
16
|
+
return iso;
|
|
16
17
|
}
|
|
17
|
-
return null;
|
|
18
18
|
};
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* locale and i18n, with no client bundle. The built-in field types are formatted directly; threading
|
|
23
|
-
* the full resolved registry (custom types) to the view is a future enhancement.
|
|
20
|
+
* Server component. Formats submission data into serializable props and delegates
|
|
21
|
+
* rendering to the client component which uses Payload's UI components.
|
|
24
22
|
*/
|
|
25
23
|
const SubmissionAnswers = ({ data, req }) => {
|
|
26
24
|
const doc = data ?? {};
|
|
@@ -28,27 +26,105 @@ const SubmissionAnswers = ({ data, req }) => {
|
|
|
28
26
|
const values = doc.values ?? [];
|
|
29
27
|
const locale = doc.locale ?? req.locale ?? "en";
|
|
30
28
|
const t = asFieldTranslate(req.i18n.t);
|
|
31
|
-
|
|
29
|
+
const rawConsent = doc.consent ?? [];
|
|
30
|
+
const meta = doc.meta;
|
|
32
31
|
const valueByField = new Map(values.map((entry) => [entry.field, entry.value]));
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
32
|
+
const answers = [];
|
|
33
|
+
const repeaters = [];
|
|
34
|
+
for (const descriptor of descriptors) {
|
|
35
|
+
const raw = valueByField.get(descriptor.field);
|
|
36
|
+
if (descriptor.fieldType === "repeater") {
|
|
37
|
+
const rows = Array.isArray(raw) ? raw : [];
|
|
38
|
+
const subDescs = descriptor.subFieldDescriptors ?? [];
|
|
39
|
+
repeaters.push({
|
|
40
|
+
field: descriptor.field,
|
|
41
|
+
label: descriptor.label,
|
|
42
|
+
rows: rows.map((row, rowIndex) => ({
|
|
43
|
+
id: String(rowIndex),
|
|
44
|
+
subFields: subDescs.map((sub) => {
|
|
45
|
+
const subDef = registry.get(sub.fieldType);
|
|
46
|
+
const subRaw = row[sub.field];
|
|
47
|
+
const formatted = subDef?.format ? subDef.format({
|
|
48
|
+
value: subRaw,
|
|
49
|
+
config: {},
|
|
50
|
+
optionLabels: sub.optionLabels,
|
|
51
|
+
locale,
|
|
52
|
+
t
|
|
53
|
+
}) : subRaw == null ? "" : String(subRaw);
|
|
54
|
+
return {
|
|
55
|
+
label: sub.label,
|
|
56
|
+
value: formatted || "—"
|
|
57
|
+
};
|
|
58
|
+
})
|
|
59
|
+
}))
|
|
60
|
+
});
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const def = registry.get(descriptor.fieldType);
|
|
64
|
+
const formatted = def?.format ? def.format({
|
|
65
|
+
value: raw,
|
|
66
|
+
config: {},
|
|
67
|
+
optionLabels: descriptor.optionLabels,
|
|
68
|
+
locale,
|
|
69
|
+
t
|
|
70
|
+
}) : raw == null ? "" : String(raw);
|
|
71
|
+
if (descriptor.fieldType === "file") {
|
|
72
|
+
const ref = raw && typeof raw === "object" && "url" in raw ? raw : null;
|
|
73
|
+
const url = typeof ref?.url === "string" ? ref.url : void 0;
|
|
74
|
+
const filename = typeof ref?.filename === "string" ? ref.filename : url ?? "";
|
|
75
|
+
answers.push({
|
|
76
|
+
field: descriptor.field,
|
|
77
|
+
label: descriptor.label,
|
|
78
|
+
value: filename,
|
|
79
|
+
href: url
|
|
80
|
+
});
|
|
81
|
+
} else answers.push({
|
|
82
|
+
field: descriptor.field,
|
|
83
|
+
label: descriptor.label,
|
|
84
|
+
value: formatted || "—",
|
|
85
|
+
multiline: descriptor.fieldType === "textarea"
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
const consent = rawConsent.map((entry) => ({
|
|
89
|
+
...entry,
|
|
90
|
+
at: formatDate(entry.at, locale)
|
|
91
|
+
}));
|
|
92
|
+
const metaItems = [];
|
|
93
|
+
if (meta) {
|
|
94
|
+
if (meta.at) metaItems.push({
|
|
95
|
+
label: t(keys.submissionMetaReceivedAt),
|
|
96
|
+
value: formatDate(meta.at, locale)
|
|
97
|
+
});
|
|
98
|
+
if (meta.ip) metaItems.push({
|
|
99
|
+
label: t(keys.submissionMetaIp),
|
|
100
|
+
value: meta.ip
|
|
101
|
+
});
|
|
102
|
+
if (meta.ua) metaItems.push({
|
|
103
|
+
label: t(keys.submissionMetaUserAgent),
|
|
104
|
+
value: String(meta.ua)
|
|
105
|
+
});
|
|
106
|
+
if (meta.spam) {
|
|
107
|
+
const captcha = typeof meta.spam === "object" && "captcha" in meta.spam ? String(meta.spam.captcha) : "—";
|
|
108
|
+
metaItems.push({
|
|
109
|
+
label: t(keys.submissionMetaCaptcha),
|
|
110
|
+
value: captcha
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return /* @__PURE__ */ jsx(SubmissionAnswersClient, {
|
|
115
|
+
answers,
|
|
116
|
+
repeaters,
|
|
117
|
+
consent,
|
|
118
|
+
meta: metaItems,
|
|
119
|
+
labels: {
|
|
120
|
+
answers: t(keys.submissionAnswers),
|
|
121
|
+
consent: t(keys.submissionConsent),
|
|
122
|
+
submissionDetails: t(keys.submissionDetails),
|
|
123
|
+
agreed: t(keys.submissionConsentAgreed),
|
|
124
|
+
declined: t(keys.submissionConsentDeclined),
|
|
125
|
+
row: t(keys.repeaterRow),
|
|
126
|
+
empty: t(keys.submissionNoAnswers)
|
|
127
|
+
}
|
|
52
128
|
});
|
|
53
129
|
};
|
|
54
130
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SubmissionAnswers.js","names":[],"sources":["../../src/submissions/SubmissionAnswers.tsx"],"sourcesContent":["import type { UIFieldServerProps } from 'payload'\nimport { defaultFieldDefinitions } from '../fields/builtin'\nimport { buildRegistry } from '../fields/registry'\nimport { keys } from '../translations/keys'\nimport { asFieldTranslate } from '../translations/server'\nimport type { SubmissionDescriptor, SubmissionValue } from './types'\n\nconst registry = buildRegistry(defaultFieldDefinitions)\n\ntype
|
|
1
|
+
{"version":3,"file":"SubmissionAnswers.js","names":[],"sources":["../../src/submissions/SubmissionAnswers.tsx"],"sourcesContent":["import type { UIFieldServerProps } from 'payload'\nimport { defaultFieldDefinitions } from '../fields/builtin'\nimport { buildRegistry } from '../fields/registry'\nimport { keys } from '../translations/keys'\nimport { asFieldTranslate } from '../translations/server'\nimport type {\n\tAnswerItem,\n\tConsentItem,\n\tMetaItem,\n\tRepeaterItem,\n\tSubmissionAnswersLabels,\n} from './SubmissionAnswersClient'\nimport { SubmissionAnswersClient } from './SubmissionAnswersClient'\nimport type { SubmissionDescriptor, SubmissionValue } from './types'\n\nconst registry = buildRegistry(defaultFieldDefinitions)\n\ntype ConsentEntry = {\n\tfield: string\n\tagreed: boolean\n\tref?: string\n\tversionRef?: string\n\tat: string\n}\n\ntype MetaSpam = { captcha?: string }\n\ntype SubmissionMeta = {\n\tat?: string\n\tip?: string\n\tua?: string\n\tspam?: MetaSpam\n\t[key: string]: unknown\n}\n\ntype SubmissionDoc = {\n\tvalues?: SubmissionValue[]\n\tdescriptors?: SubmissionDescriptor[]\n\tlocale?: string\n\tconsent?: ConsentEntry[]\n\tmeta?: SubmissionMeta\n}\n\nconst formatDate = (iso: string, locale: string): string => {\n\ttry {\n\t\treturn new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeStyle: 'short' }).format(\n\t\t\tnew Date(iso)\n\t\t)\n\t} catch {\n\t\treturn iso\n\t}\n}\n\n/**\n * Server component. Formats submission data into serializable props and delegates\n * rendering to the client component which uses Payload's UI components.\n */\nexport const SubmissionAnswers = ({ data, req }: UIFieldServerProps) => {\n\tconst doc = (data ?? {}) as SubmissionDoc\n\tconst descriptors = doc.descriptors ?? []\n\tconst values = doc.values ?? []\n\tconst locale = doc.locale ?? req.locale ?? 'en'\n\tconst t = asFieldTranslate(req.i18n.t)\n\tconst rawConsent = doc.consent ?? []\n\tconst meta = doc.meta\n\n\tconst valueByField = new Map(values.map((entry) => [entry.field, entry.value]))\n\n\tconst answers: AnswerItem[] = []\n\tconst repeaters: RepeaterItem[] = []\n\n\tfor (const descriptor of descriptors) {\n\t\tconst raw = valueByField.get(descriptor.field)\n\n\t\tif (descriptor.fieldType === 'repeater') {\n\t\t\tconst rows = Array.isArray(raw) ? (raw as Array<Record<string, unknown>>) : []\n\t\t\tconst subDescs = descriptor.subFieldDescriptors ?? []\n\t\t\trepeaters.push({\n\t\t\t\tfield: descriptor.field,\n\t\t\t\tlabel: descriptor.label,\n\t\t\t\trows: rows.map((row, rowIndex) => ({\n\t\t\t\t\tid: String(rowIndex),\n\t\t\t\t\tsubFields: subDescs.map((sub) => {\n\t\t\t\t\t\tconst subDef = registry.get(sub.fieldType)\n\t\t\t\t\t\tconst subRaw = row[sub.field]\n\t\t\t\t\t\tconst formatted = subDef?.format\n\t\t\t\t\t\t\t? subDef.format({\n\t\t\t\t\t\t\t\t\tvalue: subRaw,\n\t\t\t\t\t\t\t\t\tconfig: {},\n\t\t\t\t\t\t\t\t\toptionLabels: sub.optionLabels,\n\t\t\t\t\t\t\t\t\tlocale,\n\t\t\t\t\t\t\t\t\tt,\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t: subRaw == null\n\t\t\t\t\t\t\t\t? ''\n\t\t\t\t\t\t\t\t: String(subRaw)\n\t\t\t\t\t\treturn { label: sub.label, value: formatted || '—' }\n\t\t\t\t\t}),\n\t\t\t\t})),\n\t\t\t})\n\t\t\tcontinue\n\t\t}\n\n\t\tconst def = registry.get(descriptor.fieldType)\n\t\tconst formatted = def?.format\n\t\t\t? def.format({ value: raw, config: {}, optionLabels: descriptor.optionLabels, locale, t })\n\t\t\t: raw == null\n\t\t\t\t? ''\n\t\t\t\t: String(raw)\n\n\t\tif (descriptor.fieldType === 'file') {\n\t\t\tconst ref =\n\t\t\t\traw && typeof raw === 'object' && 'url' in raw\n\t\t\t\t\t? (raw as { url?: string; filename?: string })\n\t\t\t\t\t: null\n\t\t\tconst url = typeof ref?.url === 'string' ? ref.url : undefined\n\t\t\tconst filename = typeof ref?.filename === 'string' ? ref.filename : (url ?? '')\n\t\t\tanswers.push({ field: descriptor.field, label: descriptor.label, value: filename, href: url })\n\t\t} else {\n\t\t\tanswers.push({\n\t\t\t\tfield: descriptor.field,\n\t\t\t\tlabel: descriptor.label,\n\t\t\t\tvalue: formatted || '—',\n\t\t\t\tmultiline: descriptor.fieldType === 'textarea',\n\t\t\t})\n\t\t}\n\t}\n\n\tconst consent: ConsentItem[] = rawConsent.map((entry) => ({\n\t\t...entry,\n\t\tat: formatDate(entry.at, locale),\n\t}))\n\n\tconst metaItems: MetaItem[] = []\n\tif (meta) {\n\t\tif (meta.at)\n\t\t\tmetaItems.push({\n\t\t\t\tlabel: t(keys.submissionMetaReceivedAt),\n\t\t\t\tvalue: formatDate(meta.at, locale),\n\t\t\t})\n\t\tif (meta.ip) metaItems.push({ label: t(keys.submissionMetaIp), value: meta.ip })\n\t\tif (meta.ua) metaItems.push({ label: t(keys.submissionMetaUserAgent), value: String(meta.ua) })\n\t\tif (meta.spam) {\n\t\t\tconst captcha =\n\t\t\t\ttypeof meta.spam === 'object' && 'captcha' in meta.spam\n\t\t\t\t\t? String((meta.spam as MetaSpam).captcha)\n\t\t\t\t\t: '—'\n\t\t\tmetaItems.push({ label: t(keys.submissionMetaCaptcha), value: captcha })\n\t\t}\n\t}\n\n\tconst labels: SubmissionAnswersLabels = {\n\t\tanswers: t(keys.submissionAnswers),\n\t\tconsent: t(keys.submissionConsent),\n\t\tsubmissionDetails: t(keys.submissionDetails),\n\t\tagreed: t(keys.submissionConsentAgreed),\n\t\tdeclined: t(keys.submissionConsentDeclined),\n\t\trow: t(keys.repeaterRow),\n\t\tempty: t(keys.submissionNoAnswers),\n\t}\n\n\treturn (\n\t\t<SubmissionAnswersClient\n\t\t\tanswers={answers}\n\t\t\trepeaters={repeaters}\n\t\t\tconsent={consent}\n\t\t\tmeta={metaItems}\n\t\t\tlabels={labels}\n\t\t/>\n\t)\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { FieldLabel, TextInput } from "@payloadcms/ui";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
//#region src/submissions/SubmissionAnswersClient.tsx
|
|
5
|
+
const noop = (_e) => {};
|
|
6
|
+
const pillStyle = (agreed) => ({
|
|
7
|
+
display: "inline-block",
|
|
8
|
+
padding: "2px 8px",
|
|
9
|
+
borderRadius: "9999px",
|
|
10
|
+
fontSize: "0.75rem",
|
|
11
|
+
fontWeight: 600,
|
|
12
|
+
background: agreed ? "var(--theme-success-500)" : "var(--theme-error-500)",
|
|
13
|
+
color: "#fff"
|
|
14
|
+
});
|
|
15
|
+
const rowHeading = {
|
|
16
|
+
fontSize: "0.75rem",
|
|
17
|
+
fontWeight: 600,
|
|
18
|
+
color: "var(--theme-elevation-500)",
|
|
19
|
+
margin: "0 0 4px"
|
|
20
|
+
};
|
|
21
|
+
const SubmissionAnswersClient = ({ answers, repeaters, consent, meta, labels }) => {
|
|
22
|
+
if (!(answers.length > 0 || repeaters.length > 0 || consent.length > 0 || meta.length > 0)) return /* @__PURE__ */ jsx("p", {
|
|
23
|
+
style: { color: "var(--theme-elevation-500)" },
|
|
24
|
+
children: labels.empty
|
|
25
|
+
});
|
|
26
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
27
|
+
className: "field-type",
|
|
28
|
+
style: {
|
|
29
|
+
display: "flex",
|
|
30
|
+
flexDirection: "column",
|
|
31
|
+
gap: "32px"
|
|
32
|
+
},
|
|
33
|
+
children: [
|
|
34
|
+
(answers.length > 0 || repeaters.length > 0) && /* @__PURE__ */ jsxs("section", { children: [
|
|
35
|
+
/* @__PURE__ */ jsx(FieldLabel, { label: labels.answers }),
|
|
36
|
+
answers.map(({ field, label, value, href }) => href ? /* @__PURE__ */ jsxs("div", {
|
|
37
|
+
className: "field-type",
|
|
38
|
+
children: [/* @__PURE__ */ jsx(FieldLabel, { label }), /* @__PURE__ */ jsx("a", {
|
|
39
|
+
href,
|
|
40
|
+
target: "_blank",
|
|
41
|
+
rel: "noreferrer",
|
|
42
|
+
style: {
|
|
43
|
+
display: "block",
|
|
44
|
+
padding: "8px 0",
|
|
45
|
+
color: "var(--theme-text)",
|
|
46
|
+
textDecoration: "underline"
|
|
47
|
+
},
|
|
48
|
+
children: value
|
|
49
|
+
})]
|
|
50
|
+
}, field) : /* @__PURE__ */ jsx(TextInput, {
|
|
51
|
+
path: `sa-${field}`,
|
|
52
|
+
label,
|
|
53
|
+
value,
|
|
54
|
+
readOnly: true,
|
|
55
|
+
onChange: noop
|
|
56
|
+
}, field)),
|
|
57
|
+
repeaters.map(({ field, label, rows }) => /* @__PURE__ */ jsxs("div", {
|
|
58
|
+
className: "field-type",
|
|
59
|
+
children: [/* @__PURE__ */ jsx(FieldLabel, { label }), rows.length === 0 ? /* @__PURE__ */ jsx(TextInput, {
|
|
60
|
+
path: `sa-${field}-empty`,
|
|
61
|
+
label: "",
|
|
62
|
+
value: "—",
|
|
63
|
+
readOnly: true,
|
|
64
|
+
onChange: noop
|
|
65
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
66
|
+
style: {
|
|
67
|
+
display: "flex",
|
|
68
|
+
flexDirection: "column",
|
|
69
|
+
gap: "8px",
|
|
70
|
+
marginTop: "4px"
|
|
71
|
+
},
|
|
72
|
+
children: rows.map((row) => /* @__PURE__ */ jsxs("div", {
|
|
73
|
+
style: {
|
|
74
|
+
border: "1px solid var(--theme-elevation-150)",
|
|
75
|
+
borderRadius: "4px",
|
|
76
|
+
padding: "4px 12px 0"
|
|
77
|
+
},
|
|
78
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
79
|
+
style: rowHeading,
|
|
80
|
+
children: labels.row.replace("{n}", String(Number(row.id) + 1))
|
|
81
|
+
}), row.subFields.map((subField) => /* @__PURE__ */ jsx(TextInput, {
|
|
82
|
+
path: `sa-${field}-${row.id}-${subField.label}`,
|
|
83
|
+
label: subField.label,
|
|
84
|
+
value: subField.value,
|
|
85
|
+
readOnly: true,
|
|
86
|
+
onChange: noop
|
|
87
|
+
}, subField.label))]
|
|
88
|
+
}, row.id))
|
|
89
|
+
})]
|
|
90
|
+
}, field))
|
|
91
|
+
] }),
|
|
92
|
+
consent.length > 0 && /* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx(FieldLabel, { label: labels.consent }), consent.map((entry) => /* @__PURE__ */ jsxs("div", {
|
|
93
|
+
className: "field-type",
|
|
94
|
+
children: [/* @__PURE__ */ jsx(FieldLabel, { label: entry.field }), /* @__PURE__ */ jsxs("div", {
|
|
95
|
+
style: {
|
|
96
|
+
display: "flex",
|
|
97
|
+
flexDirection: "column",
|
|
98
|
+
gap: "4px"
|
|
99
|
+
},
|
|
100
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
101
|
+
style: {
|
|
102
|
+
display: "flex",
|
|
103
|
+
alignItems: "center",
|
|
104
|
+
gap: "8px",
|
|
105
|
+
flexWrap: "wrap"
|
|
106
|
+
},
|
|
107
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
108
|
+
style: pillStyle(entry.agreed),
|
|
109
|
+
children: entry.agreed ? labels.agreed : labels.declined
|
|
110
|
+
}), entry.ref ? /* @__PURE__ */ jsxs("a", {
|
|
111
|
+
href: entry.ref,
|
|
112
|
+
target: "_blank",
|
|
113
|
+
rel: "noreferrer",
|
|
114
|
+
style: {
|
|
115
|
+
fontSize: "0.8rem",
|
|
116
|
+
color: "var(--theme-text)"
|
|
117
|
+
},
|
|
118
|
+
children: [entry.ref, entry.versionRef ? ` (v${entry.versionRef})` : ""]
|
|
119
|
+
}) : null]
|
|
120
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
121
|
+
style: {
|
|
122
|
+
fontSize: "0.75rem",
|
|
123
|
+
color: "var(--theme-elevation-500)"
|
|
124
|
+
},
|
|
125
|
+
children: entry.at
|
|
126
|
+
})]
|
|
127
|
+
})]
|
|
128
|
+
}, entry.field))] }),
|
|
129
|
+
meta.length > 0 && /* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx(FieldLabel, { label: labels.submissionDetails }), meta.map((item) => /* @__PURE__ */ jsx(TextInput, {
|
|
130
|
+
path: `sa-meta-${item.label}`,
|
|
131
|
+
label: item.label,
|
|
132
|
+
value: item.value,
|
|
133
|
+
readOnly: true,
|
|
134
|
+
onChange: noop
|
|
135
|
+
}, item.label))] })
|
|
136
|
+
]
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
//#endregion
|
|
140
|
+
export { SubmissionAnswersClient };
|
|
141
|
+
|
|
142
|
+
//# sourceMappingURL=SubmissionAnswersClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SubmissionAnswersClient.js","names":[],"sources":["../../src/submissions/SubmissionAnswersClient.tsx"],"sourcesContent":["'use client'\n\nimport { FieldLabel, TextInput } from '@payloadcms/ui'\nimport type { ChangeEvent } from 'react'\n\nexport type AnswerItem = {\n\tfield: string\n\tlabel: string\n\tvalue: string\n\thref?: string\n\tmultiline?: boolean\n}\n\nexport type RepeaterRow = {\n\tid: string\n\tsubFields: Array<{ label: string; value: string }>\n}\n\nexport type RepeaterItem = {\n\tfield: string\n\tlabel: string\n\trows: RepeaterRow[]\n}\n\nexport type ConsentItem = {\n\tfield: string\n\tagreed: boolean\n\tref?: string\n\tversionRef?: string\n\tat: string\n}\n\nexport type MetaItem = {\n\tlabel: string\n\tvalue: string\n}\n\nexport type SubmissionAnswersLabels = {\n\tanswers: string\n\tconsent: string\n\tsubmissionDetails: string\n\tagreed: string\n\tdeclined: string\n\t/** Row heading template containing `{n}`, the 1-based row number. */\n\trow: string\n\tempty: string\n}\n\ntype Props = {\n\tanswers: AnswerItem[]\n\trepeaters: RepeaterItem[]\n\tconsent: ConsentItem[]\n\tmeta: MetaItem[]\n\tlabels: SubmissionAnswersLabels\n}\n\nconst noop = (_e: ChangeEvent<HTMLInputElement>) => {}\n\nconst pillStyle = (agreed: boolean): React.CSSProperties => ({\n\tdisplay: 'inline-block',\n\tpadding: '2px 8px',\n\tborderRadius: '9999px',\n\tfontSize: '0.75rem',\n\tfontWeight: 600,\n\tbackground: agreed ? 'var(--theme-success-500)' : 'var(--theme-error-500)',\n\tcolor: '#fff',\n})\n\nconst rowHeading: React.CSSProperties = {\n\tfontSize: '0.75rem',\n\tfontWeight: 600,\n\tcolor: 'var(--theme-elevation-500)',\n\tmargin: '0 0 4px',\n}\n\nexport const SubmissionAnswersClient = ({ answers, repeaters, consent, meta, labels }: Props) => {\n\tconst hasContent =\n\t\tanswers.length > 0 || repeaters.length > 0 || consent.length > 0 || meta.length > 0\n\n\tif (!hasContent) {\n\t\treturn <p style={{ color: 'var(--theme-elevation-500)' }}>{labels.empty}</p>\n\t}\n\n\treturn (\n\t\t<div className=\"field-type\" style={{ display: 'flex', flexDirection: 'column', gap: '32px' }}>\n\t\t\t{(answers.length > 0 || repeaters.length > 0) && (\n\t\t\t\t<section>\n\t\t\t\t\t<FieldLabel label={labels.answers} />\n\t\t\t\t\t{answers.map(({ field, label, value, href }) =>\n\t\t\t\t\t\thref ? (\n\t\t\t\t\t\t\t<div key={field} className=\"field-type\">\n\t\t\t\t\t\t\t\t<FieldLabel label={label} />\n\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\thref={href}\n\t\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\t\trel=\"noreferrer\"\n\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\tdisplay: 'block',\n\t\t\t\t\t\t\t\t\t\tpadding: '8px 0',\n\t\t\t\t\t\t\t\t\t\tcolor: 'var(--theme-text)',\n\t\t\t\t\t\t\t\t\t\ttextDecoration: 'underline',\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\t\t{value}\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t<TextInput\n\t\t\t\t\t\t\t\tkey={field}\n\t\t\t\t\t\t\t\tpath={`sa-${field}`}\n\t\t\t\t\t\t\t\tlabel={label}\n\t\t\t\t\t\t\t\tvalue={value}\n\t\t\t\t\t\t\t\treadOnly\n\t\t\t\t\t\t\t\tonChange={noop}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t)\n\t\t\t\t\t)}\n\n\t\t\t\t\t{repeaters.map(({ field, label, rows }) => (\n\t\t\t\t\t\t<div key={field} className=\"field-type\">\n\t\t\t\t\t\t\t<FieldLabel label={label} />\n\t\t\t\t\t\t\t{rows.length === 0 ? (\n\t\t\t\t\t\t\t\t<TextInput path={`sa-${field}-empty`} label=\"\" value=\"—\" readOnly onChange={noop} />\n\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\tstyle={{ display: 'flex', flexDirection: 'column', gap: '8px', marginTop: '4px' }}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{rows.map((row) => (\n\t\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\t\tkey={row.id}\n\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\tborder: '1px solid var(--theme-elevation-150)',\n\t\t\t\t\t\t\t\t\t\t\t\tborderRadius: '4px',\n\t\t\t\t\t\t\t\t\t\t\t\tpadding: '4px 12px 0',\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\t\t<p style={rowHeading}>\n\t\t\t\t\t\t\t\t\t\t\t\t{labels.row.replace('{n}', String(Number(row.id) + 1))}\n\t\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t\t\t{row.subFields.map((subField) => (\n\t\t\t\t\t\t\t\t\t\t\t\t<TextInput\n\t\t\t\t\t\t\t\t\t\t\t\t\tkey={subField.label}\n\t\t\t\t\t\t\t\t\t\t\t\t\tpath={`sa-${field}-${row.id}-${subField.label}`}\n\t\t\t\t\t\t\t\t\t\t\t\t\tlabel={subField.label}\n\t\t\t\t\t\t\t\t\t\t\t\t\tvalue={subField.value}\n\t\t\t\t\t\t\t\t\t\t\t\t\treadOnly\n\t\t\t\t\t\t\t\t\t\t\t\t\tonChange={noop}\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</div>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t))}\n\t\t\t\t</section>\n\t\t\t)}\n\n\t\t\t{consent.length > 0 && (\n\t\t\t\t<section>\n\t\t\t\t\t<FieldLabel label={labels.consent} />\n\t\t\t\t\t{consent.map((entry) => (\n\t\t\t\t\t\t<div key={entry.field} className=\"field-type\">\n\t\t\t\t\t\t\t<FieldLabel label={entry.field} />\n\t\t\t\t\t\t\t<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>\n\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\tstyle={{ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<span style={pillStyle(entry.agreed)}>\n\t\t\t\t\t\t\t\t\t\t{entry.agreed ? labels.agreed : labels.declined}\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t{entry.ref ? (\n\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\thref={entry.ref}\n\t\t\t\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\t\t\t\trel=\"noreferrer\"\n\t\t\t\t\t\t\t\t\t\t\tstyle={{ fontSize: '0.8rem', color: 'var(--theme-text)' }}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t{entry.ref}\n\t\t\t\t\t\t\t\t\t\t\t{entry.versionRef ? ` (v${entry.versionRef})` : ''}\n\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<span style={{ fontSize: '0.75rem', color: 'var(--theme-elevation-500)' }}>\n\t\t\t\t\t\t\t\t\t{entry.at}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t))}\n\t\t\t\t</section>\n\t\t\t)}\n\n\t\t\t{meta.length > 0 && (\n\t\t\t\t<section>\n\t\t\t\t\t<FieldLabel label={labels.submissionDetails} />\n\t\t\t\t\t{meta.map((item) => (\n\t\t\t\t\t\t<TextInput\n\t\t\t\t\t\t\tkey={item.label}\n\t\t\t\t\t\t\tpath={`sa-meta-${item.label}`}\n\t\t\t\t\t\t\tlabel={item.label}\n\t\t\t\t\t\t\tvalue={item.value}\n\t\t\t\t\t\t\treadOnly\n\t\t\t\t\t\t\tonChange={noop}\n\t\t\t\t\t\t/>\n\t\t\t\t\t))}\n\t\t\t\t</section>\n\t\t\t)}\n\t\t</div>\n\t)\n}\n"],"mappings":"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"}
|
|
@@ -66,8 +66,8 @@ const runSubmission = async (input) => {
|
|
|
66
66
|
const definition = registry.get(instance.blockType);
|
|
67
67
|
const raw = incoming.get(instance.name);
|
|
68
68
|
if (!definition || calcExpressionOf(instance)) continue;
|
|
69
|
-
const effectiveRaw = instance.blockType === "consent" && isEmpty(raw) ? false : raw;
|
|
70
|
-
if (isEmpty(effectiveRaw)) continue;
|
|
69
|
+
const effectiveRaw = instance.blockType === "consent" && isEmpty(raw) ? false : instance.blockType === "repeater" && isEmpty(raw) ? [] : raw;
|
|
70
|
+
if (isEmpty(effectiveRaw) && instance.blockType !== "repeater") continue;
|
|
71
71
|
const value = coerce(definition.value, effectiveRaw);
|
|
72
72
|
coercedAnswers[instance.name] = value;
|
|
73
73
|
coercedByName.set(instance.name, value);
|
|
@@ -179,6 +179,58 @@ const runSubmission = async (input) => {
|
|
|
179
179
|
});
|
|
180
180
|
continue;
|
|
181
181
|
}
|
|
182
|
+
if (instance.blockType === "repeater") {
|
|
183
|
+
const rows = Array.isArray(value) ? value : [];
|
|
184
|
+
const subFields = Array.isArray(instance.subFields) ? instance.subFields : [];
|
|
185
|
+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
|
|
186
|
+
const row = rows[rowIndex] ?? {};
|
|
187
|
+
for (const subField of subFields) {
|
|
188
|
+
const subDef = registry.get(subField.blockType);
|
|
189
|
+
if (!subDef) continue;
|
|
190
|
+
if (!evaluateCondition(subField.visibleWhen, row)) continue;
|
|
191
|
+
if (!evaluateCondition(subField.validateWhen, row)) continue;
|
|
192
|
+
const subValue = row[subField.name];
|
|
193
|
+
const { errors: subErrors } = await runValidation({
|
|
194
|
+
field: subField,
|
|
195
|
+
fieldDefinition: subDef,
|
|
196
|
+
value: subValue,
|
|
197
|
+
fieldType: subField.blockType,
|
|
198
|
+
ruleRegistry,
|
|
199
|
+
answers: row,
|
|
200
|
+
locale,
|
|
201
|
+
t,
|
|
202
|
+
operation,
|
|
203
|
+
event: "submit",
|
|
204
|
+
mode: "server",
|
|
205
|
+
req,
|
|
206
|
+
payload,
|
|
207
|
+
formId
|
|
208
|
+
});
|
|
209
|
+
for (const issue of subErrors.filter((e) => e.severity === "error")) errors.push({
|
|
210
|
+
path: `${instance.name}[${rowIndex}].${subField.name}`,
|
|
211
|
+
message: issue.message
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (errors.length > 0) continue;
|
|
216
|
+
const subFieldDescriptors = subFields.map((sf) => ({
|
|
217
|
+
field: sf.name,
|
|
218
|
+
label: sf.label ?? sf.name,
|
|
219
|
+
fieldType: sf.blockType,
|
|
220
|
+
...sf.options ? { optionLabels: optionLabelsFor(sf) ?? void 0 } : {}
|
|
221
|
+
}));
|
|
222
|
+
outValues.push({
|
|
223
|
+
field: instance.name,
|
|
224
|
+
value: rows
|
|
225
|
+
});
|
|
226
|
+
descriptors.push({
|
|
227
|
+
field: instance.name,
|
|
228
|
+
label: instance.label ?? instance.name,
|
|
229
|
+
fieldType: instance.blockType,
|
|
230
|
+
...subFieldDescriptors.length > 0 ? { subFieldDescriptors } : {}
|
|
231
|
+
});
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
182
234
|
if (isEmpty(raw)) continue;
|
|
183
235
|
outValues.push({
|
|
184
236
|
field: instance.name,
|