@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.
Files changed (49) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +3 -1
  3. package/dist/collections/formSubmissions.js +57 -38
  4. package/dist/collections/formSubmissions.js.map +1 -1
  5. package/dist/collections/forms.js +125 -114
  6. package/dist/collections/forms.js.map +1 -1
  7. package/dist/collections/uploads.d.ts +2 -1
  8. package/dist/collections/uploads.js +22 -13
  9. package/dist/collections/uploads.js.map +1 -1
  10. package/dist/exports/react.d.ts +2 -2
  11. package/dist/exports/react.js +1 -1
  12. package/dist/exports/types.d.ts +2 -1
  13. package/dist/fields/buildFieldBlocks.js +14 -0
  14. package/dist/fields/buildFieldBlocks.js.map +1 -1
  15. package/dist/fields/builtin/index.js +3 -1
  16. package/dist/fields/builtin/index.js.map +1 -1
  17. package/dist/fields/builtin/repeater.js +44 -0
  18. package/dist/fields/builtin/repeater.js.map +1 -0
  19. package/dist/fields/types.d.ts +2 -1
  20. package/dist/index.d.ts +19 -0
  21. package/dist/index.js +3 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/plugin/collectionOverrides.d.ts +22 -0
  24. package/dist/plugin/registerCollections.js +37 -9
  25. package/dist/plugin/registerCollections.js.map +1 -1
  26. package/dist/react/Form.d.ts +32 -3
  27. package/dist/react/Form.js +47 -5
  28. package/dist/react/Form.js.map +1 -1
  29. package/dist/react/FormContext.js.map +1 -1
  30. package/dist/react/renderers/index.js +2 -0
  31. package/dist/react/renderers/index.js.map +1 -1
  32. package/dist/react/renderers/repeater.js +113 -0
  33. package/dist/react/renderers/repeater.js.map +1 -0
  34. package/dist/submissions/SubmissionAnswers.d.ts +2 -4
  35. package/dist/submissions/SubmissionAnswers.js +110 -34
  36. package/dist/submissions/SubmissionAnswers.js.map +1 -1
  37. package/dist/submissions/SubmissionAnswersClient.js +142 -0
  38. package/dist/submissions/SubmissionAnswersClient.js.map +1 -0
  39. package/dist/submissions/runSubmission.js +54 -2
  40. package/dist/submissions/runSubmission.js.map +1 -1
  41. package/dist/submissions/types.d.ts +2 -1
  42. package/dist/translations/en.js +26 -1
  43. package/dist/translations/en.js.map +1 -1
  44. package/dist/translations/keys.d.ts +25 -0
  45. package/dist/translations/keys.js +26 -1
  46. package/dist/translations/keys.js.map +1 -1
  47. package/dist/validation/runValidation.js +1 -1
  48. package/dist/validation/runValidation.js.map +1 -1
  49. package/package.json +3 -3
@@ -12,7 +12,7 @@ import { FormPresentation, PresentationWrapperProps } from "../react/presentatio
12
12
  import { PresentationOption, PresentationRegistry, PresentationsConfig, resolvePresentations } from "../react/presentation/registry.js";
13
13
  import { RendererOption, RendererRegistry, RenderersConfig, resolveRenderers } from "../react/registry.js";
14
14
  import { SubmitFormResult, SubmitHandler, submitForm } from "../react/submitForm.js";
15
- import { Form, FormDocument, FormProps } from "../react/Form.js";
15
+ import { BackButtonRenderProps, Form, FormDocument, FormProps, NextButtonRenderProps, SubmitButtonRenderProps } from "../react/Form.js";
16
16
  import { FormState } from "../react/state.js";
17
17
  import { FormStepInfo } from "../react/FormContext.js";
18
18
  import { FieldWidth, FormLayout, FormLayoutProps, widthProps } from "../react/FormLayout.js";
@@ -42,4 +42,4 @@ import { RecallResolver, buildRecallResolver } from "../recall/resolver.js";
42
42
  import { CAPTCHA_TOKEN_KEY, DEFAULT_HONEYPOT_FIELD } from "../spam/constants.js";
43
43
  import { en } from "../translations/en.js";
44
44
  import { makeTranslate } from "../translations/makeTranslate.js";
45
- export { type AggregationBucket, Backdrop, type BackdropProps, CAPTCHA_TOKEN_KEY, type CalcExpression, Checkbox, type CheckboxProps, DEFAULT_HONEYPOT_FIELD, DialogSurface, type DialogSurfaceProps, type FetchResultsInput, type FetchResultsResult, type FieldAggregation, type FieldRenderer, type FieldRendererProps, FieldShell, type FieldShellProps, type FieldWidth, Form, type FormDocument, type FormFieldInstance, FormLayout, type FormLayoutProps, type FormPresentation, type FormProps, FormResults, type FormResultsProps, type FormState, type FormStepInfo, Honeypot, type HoneypotProps, Input, type InputProps, Poll, type PollProps, type PrefillOptions, type PresentationOption, type PresentationRegistry, type PresentationWrapperProps, type PresentationsConfig, type RecallResolver, type RendererOption, type RendererRegistry, type RendererTranslate, type RenderersConfig, Select, type SelectOption, type SelectProps, type SubmitFormResult, type SubmitHandler, Textarea, type TextareaProps, type UploadFileInput, type UploadFileResult, type UseDismissOptions, type UseFieldResult, buildRecallResolver, cn, computeCalcFields, defaultPresentations, defaultRenderers, defineFieldRenderer, en, evaluateCalc, evaluateCondition, fetchFormResults, firstStepId, interpolate, isTerminalStepId, makeTranslate, resolveNextStepId, resolvePresentations, resolveRenderers, stepFieldNames, submitForm, toFormDocument, uploadFile, useDismiss, useField, useFocusTrap, useFormState, useFormStep, useScrollLock, valuesFromSearchParams, widthProps };
45
+ export { type AggregationBucket, type BackButtonRenderProps, Backdrop, type BackdropProps, CAPTCHA_TOKEN_KEY, type CalcExpression, Checkbox, type CheckboxProps, DEFAULT_HONEYPOT_FIELD, DialogSurface, type DialogSurfaceProps, type FetchResultsInput, type FetchResultsResult, type FieldAggregation, type FieldRenderer, type FieldRendererProps, FieldShell, type FieldShellProps, type FieldWidth, Form, type FormDocument, type FormFieldInstance, FormLayout, type FormLayoutProps, type FormPresentation, type FormProps, FormResults, type FormResultsProps, type FormState, type FormStepInfo, Honeypot, type HoneypotProps, Input, type InputProps, type NextButtonRenderProps, Poll, type PollProps, type PrefillOptions, type PresentationOption, type PresentationRegistry, type PresentationWrapperProps, type PresentationsConfig, type RecallResolver, type RendererOption, type RendererRegistry, type RendererTranslate, type RenderersConfig, Select, type SelectOption, type SelectProps, type SubmitButtonRenderProps, type SubmitFormResult, type SubmitHandler, Textarea, type TextareaProps, type UploadFileInput, type UploadFileResult, type UseDismissOptions, type UseFieldResult, buildRecallResolver, cn, computeCalcFields, defaultPresentations, defaultRenderers, defineFieldRenderer, en, evaluateCalc, evaluateCondition, fetchFormResults, firstStepId, interpolate, isTerminalStepId, makeTranslate, resolveNextStepId, resolvePresentations, resolveRenderers, stepFieldNames, submitForm, toFormDocument, uploadFile, useDismiss, useField, useFocusTrap, useFormState, useFormStep, useScrollLock, valuesFromSearchParams, widthProps };
@@ -25,11 +25,11 @@ import { FieldShell } from "../react/primitives/FieldShell.js";
25
25
  import { Checkbox } from "../react/primitives/Checkbox.js";
26
26
  import { Input } from "../react/primitives/Input.js";
27
27
  import { uploadFile } from "../react/uploadFile.js";
28
+ import { useField } from "../react/useField.js";
28
29
  import { Select } from "../react/primitives/Select.js";
29
30
  import { Textarea } from "../react/primitives/Textarea.js";
30
31
  import { defaultRenderers } from "../react/renderers/index.js";
31
32
  import { submitForm } from "../react/submitForm.js";
32
- import { useField } from "../react/useField.js";
33
33
  import { Form } from "../react/Form.js";
34
34
  import { FormResults } from "../react/FormResults.js";
35
35
  import { fetchFormResults } from "../react/fetchResults.js";
@@ -15,6 +15,7 @@ import { ActionOption, ActionRegistry, ActionsConfig } from "../actions/registry
15
15
  import { ActionResult } from "../actions/runActions.js";
16
16
  import { AggregateFieldResponsesArgs, AggregateFormResponsesArgs } from "../aggregation/aggregateResponses.js";
17
17
  import { ResolveResultsRequestArgs, ResolveResultsRequestResult } from "../aggregation/resolveResultsRequest.js";
18
+ import { CollectionOverrides, FieldsOverride } from "../plugin/collectionOverrides.js";
18
19
  import { UploadsCollectionConfig, UploadsOption } from "../collections/uploads.js";
19
20
  import { FieldCondition } from "../conditions/types.js";
20
21
  import { AnyConsentSource, ConsentLink, ConsentResolveArgs, ConsentResolved, ConsentSource } from "../consent/defineConsentSource.js";
@@ -24,4 +25,4 @@ import { PresentationDescriptorOption, PresentationDescriptorRegistry, Presentat
24
25
  import { CaptchaProvider, CaptchaVerifyArgs, IdentifyFn, RateLimitCheckArgs, RateLimitConfig, RateLimitResult, RateLimiter, SpamConfig, SpamMetadataConfig, SpamOption } from "../spam/types.js";
25
26
  import { ValidationRuleOption, ValidationRuleRegistry, ValidationRulesConfig } from "../validation/registry.js";
26
27
  import { FormBuilderPluginOptions } from "../index.js";
27
- export type { ActionDefinition, ActionOption, ActionRegistry, ActionResult, ActionRunArgs, ActionsConfig, AggregateFieldResponsesArgs, AggregateFormResponsesArgs, AggregationBucket, AggregationRow, AnyActionDefinition, AnyConsentSource, AnyFormFieldDefinition, AnyValidationRuleDefinition, CalcExpression, CaptchaProvider, CaptchaVerifyArgs, ConsentLink, ConsentProof, ConsentResolveArgs, ConsentResolved, ConsentSource, ConsentSourceOption, ConsentSourceRegistry, ConsentSourcesConfig, FieldAggregation, FieldCondition, FieldMeta, FieldTypeOption, FieldTypeRegistry, FieldTypesConfig, FileFieldConfig, FileRef, FileRefError, FlowStep, FlowTransition, FormBuilderPluginOptions, FormEvent, FormEventSink, FormFieldDefinition, FormFieldFormat, FormFieldInstance, FormFieldValidate, FormFieldValueKind, FormFlow, IdentifyFn, PrefillOptions, PresentationDensity, PresentationDescriptor, PresentationDescriptorOption, PresentationDescriptorRegistry, PresentationSurface, PresentationsDescriptorConfig, RateLimitCheckArgs, RateLimitConfig, RateLimitResult, RateLimiter, RecallResolver, ResolveResultsRequestArgs, ResolveResultsRequestResult, SpamConfig, SpamMetadataConfig, SpamOption, SubmissionDescriptor, SubmissionFieldError, SubmissionStatusFilter, SubmissionValue, UploadsCollectionConfig, UploadsOption, ValidationRuleDefinition, ValidationRuleOption, ValidationRuleRegistry, ValidationRuleResult, ValidationRulesConfig, ValidationSeverity };
28
+ export type { ActionDefinition, ActionOption, ActionRegistry, ActionResult, ActionRunArgs, ActionsConfig, AggregateFieldResponsesArgs, AggregateFormResponsesArgs, AggregationBucket, AggregationRow, AnyActionDefinition, AnyConsentSource, AnyFormFieldDefinition, AnyValidationRuleDefinition, CalcExpression, CaptchaProvider, CaptchaVerifyArgs, CollectionOverrides, ConsentLink, ConsentProof, ConsentResolveArgs, ConsentResolved, ConsentSource, ConsentSourceOption, ConsentSourceRegistry, ConsentSourcesConfig, FieldAggregation, FieldCondition, FieldMeta, FieldTypeOption, FieldTypeRegistry, FieldTypesConfig, FieldsOverride, FileFieldConfig, FileRef, FileRefError, FlowStep, FlowTransition, FormBuilderPluginOptions, FormEvent, FormEventSink, FormFieldDefinition, FormFieldFormat, FormFieldInstance, FormFieldValidate, FormFieldValueKind, FormFlow, IdentifyFn, PrefillOptions, PresentationDensity, PresentationDescriptor, PresentationDescriptorOption, PresentationDescriptorRegistry, PresentationSurface, PresentationsDescriptorConfig, RateLimitCheckArgs, RateLimitConfig, RateLimitResult, RateLimiter, RecallResolver, ResolveResultsRequestArgs, ResolveResultsRequestResult, SpamConfig, SpamMetadataConfig, SpamOption, SubmissionDescriptor, SubmissionFieldError, SubmissionStatusFilter, SubmissionValue, UploadsCollectionConfig, UploadsOption, ValidationRuleDefinition, ValidationRuleOption, ValidationRuleRegistry, ValidationRuleResult, ValidationRulesConfig, ValidationSeverity };
@@ -38,6 +38,20 @@ const buildFieldBlocks = (registry, ruleRegistry, consentRegistry) => {
38
38
  ]
39
39
  });
40
40
  }
41
+ const repeaterBlock = blocks.find((b) => b.slug === "repeater");
42
+ if (repeaterBlock) {
43
+ const subFieldBlocks = blocks.filter((b) => b.slug !== "repeater");
44
+ const fieldsArr = repeaterBlock.fields;
45
+ const validationsIdx = fieldsArr.findIndex((f) => f.name === "validations");
46
+ const subFieldsField = {
47
+ name: "subFields",
48
+ type: "blocks",
49
+ label: labelFor(keys.configSubFields),
50
+ blocks: subFieldBlocks
51
+ };
52
+ if (validationsIdx >= 0) fieldsArr.splice(validationsIdx, 0, subFieldsField);
53
+ else fieldsArr.push(subFieldsField);
54
+ }
41
55
  return blocks;
42
56
  };
43
57
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"buildFieldBlocks.js","names":[],"sources":["../../src/fields/buildFieldBlocks.ts"],"sourcesContent":["import type { Block, Field } from 'payload'\nimport { buildConditionTypeMap } from '../conditions/conditionType'\nimport { buildConsentSourceConfig } from '../consent/buildConsentSourceConfig'\nimport type { ConsentSourceRegistry } from '../consent/registry'\nimport { keys } from '../translations/keys'\nimport { labelFor } from '../translations/server'\nimport { buildRuleBlocks } from '../validation/buildRuleBlocks'\nimport type { ValidationRuleRegistry } from '../validation/registry'\nimport type { FieldTypeRegistry } from './registry'\nimport { sharedFieldConfig } from './sharedConfig'\n\n/** One add-field block per registered type: shared config, the type's own config, then its validations. */\nexport const buildFieldBlocks = (\n\tregistry: FieldTypeRegistry,\n\truleRegistry: ValidationRuleRegistry,\n\tconsentRegistry?: ConsentSourceRegistry\n): Block[] => {\n\tconst conditionTypes = buildConditionTypeMap(registry)\n\tconst blocks: Block[] = []\n\tfor (const definition of registry.values()) {\n\t\tlet typeConfig: Field[] = definition.config ?? []\n\n\t\t// Inject dynamic source select + conditional sourceConfig group for the consent field.\n\t\t// consent.ts intentionally omits source/sourceConfig; the live registry drives the\n\t\t// select options and per-source field visibility (admin.condition).\n\t\tif (definition.type === 'consent' && consentRegistry) {\n\t\t\t// statement is first; optional is last; source/sourceConfig go in between.\n\t\t\tconst statement = typeConfig.find((f) => (f as { name?: string }).name === 'statement')\n\t\t\tconst optional = typeConfig.find((f) => (f as { name?: string }).name === 'optional')\n\t\t\ttypeConfig = [\n\t\t\t\t...(statement ? [statement] : []),\n\t\t\t\t...buildConsentSourceConfig(consentRegistry),\n\t\t\t\t...(optional ? [optional] : []),\n\t\t\t]\n\t\t}\n\n\t\tblocks.push({\n\t\t\tslug: definition.type,\n\t\t\tlabels: { singular: labelFor(definition.label), plural: labelFor(definition.label) },\n\t\t\tfields: [\n\t\t\t\t...sharedFieldConfig(conditionTypes),\n\t\t\t\t...typeConfig,\n\t\t\t\t{\n\t\t\t\t\tname: 'validations',\n\t\t\t\t\ttype: 'blocks',\n\t\t\t\t\tlabel: labelFor(keys.validationsLabel),\n\t\t\t\t\tblocks: buildRuleBlocks(ruleRegistry, definition.type),\n\t\t\t\t},\n\t\t\t],\n\t\t})\n\t}\n\treturn blocks\n}\n"],"mappings":";;;;;;;;AAYA,MAAa,oBACZ,UACA,cACA,oBACa;CACb,MAAM,iBAAiB,sBAAsB,QAAQ;CACrD,MAAM,SAAkB,CAAC;CACzB,KAAK,MAAM,cAAc,SAAS,OAAO,GAAG;EAC3C,IAAI,aAAsB,WAAW,UAAU,CAAC;EAKhD,IAAI,WAAW,SAAS,aAAa,iBAAiB;GAErD,MAAM,YAAY,WAAW,MAAM,MAAO,EAAwB,SAAS,WAAW;GACtF,MAAM,WAAW,WAAW,MAAM,MAAO,EAAwB,SAAS,UAAU;GACpF,aAAa;IACZ,GAAI,YAAY,CAAC,SAAS,IAAI,CAAC;IAC/B,GAAG,yBAAyB,eAAe;IAC3C,GAAI,WAAW,CAAC,QAAQ,IAAI,CAAC;GAC9B;EACD;EAEA,OAAO,KAAK;GACX,MAAM,WAAW;GACjB,QAAQ;IAAE,UAAU,SAAS,WAAW,KAAK;IAAG,QAAQ,SAAS,WAAW,KAAK;GAAE;GACnF,QAAQ;IACP,GAAG,kBAAkB,cAAc;IACnC,GAAG;IACH;KACC,MAAM;KACN,MAAM;KACN,OAAO,SAAS,KAAK,gBAAgB;KACrC,QAAQ,gBAAgB,cAAc,WAAW,IAAI;IACtD;GACD;EACD,CAAC;CACF;CACA,OAAO;AACR"}
1
+ {"version":3,"file":"buildFieldBlocks.js","names":[],"sources":["../../src/fields/buildFieldBlocks.ts"],"sourcesContent":["import type { Block, Field } from 'payload'\nimport { buildConditionTypeMap } from '../conditions/conditionType'\nimport { buildConsentSourceConfig } from '../consent/buildConsentSourceConfig'\nimport type { ConsentSourceRegistry } from '../consent/registry'\nimport { keys } from '../translations/keys'\nimport { labelFor } from '../translations/server'\nimport { buildRuleBlocks } from '../validation/buildRuleBlocks'\nimport type { ValidationRuleRegistry } from '../validation/registry'\nimport type { FieldTypeRegistry } from './registry'\nimport { sharedFieldConfig } from './sharedConfig'\n\n/** One add-field block per registered type: shared config, the type's own config, then its validations. */\nexport const buildFieldBlocks = (\n\tregistry: FieldTypeRegistry,\n\truleRegistry: ValidationRuleRegistry,\n\tconsentRegistry?: ConsentSourceRegistry\n): Block[] => {\n\tconst conditionTypes = buildConditionTypeMap(registry)\n\tconst blocks: Block[] = []\n\tfor (const definition of registry.values()) {\n\t\tlet typeConfig: Field[] = definition.config ?? []\n\n\t\t// Inject dynamic source select + conditional sourceConfig group for the consent field.\n\t\t// consent.ts intentionally omits source/sourceConfig; the live registry drives the\n\t\t// select options and per-source field visibility (admin.condition).\n\t\tif (definition.type === 'consent' && consentRegistry) {\n\t\t\t// statement is first; optional is last; source/sourceConfig go in between.\n\t\t\tconst statement = typeConfig.find((f) => (f as { name?: string }).name === 'statement')\n\t\t\tconst optional = typeConfig.find((f) => (f as { name?: string }).name === 'optional')\n\t\t\ttypeConfig = [\n\t\t\t\t...(statement ? [statement] : []),\n\t\t\t\t...buildConsentSourceConfig(consentRegistry),\n\t\t\t\t...(optional ? [optional] : []),\n\t\t\t]\n\t\t}\n\n\t\tblocks.push({\n\t\t\tslug: definition.type,\n\t\t\tlabels: { singular: labelFor(definition.label), plural: labelFor(definition.label) },\n\t\t\tfields: [\n\t\t\t\t...sharedFieldConfig(conditionTypes),\n\t\t\t\t...typeConfig,\n\t\t\t\t{\n\t\t\t\t\tname: 'validations',\n\t\t\t\t\ttype: 'blocks',\n\t\t\t\t\tlabel: labelFor(keys.validationsLabel),\n\t\t\t\t\tblocks: buildRuleBlocks(ruleRegistry, definition.type),\n\t\t\t\t},\n\t\t\t],\n\t\t})\n\t}\n\n\t// Second pass: inject subFields into the repeater block using all non-repeater blocks.\n\t// Done after the main loop so every sibling block is already built.\n\t// Repeater-in-repeater is not supported in v1; the repeater block is excluded from subFields.\n\tconst repeaterBlock = blocks.find((b) => b.slug === 'repeater')\n\tif (repeaterBlock) {\n\t\tconst subFieldBlocks = blocks.filter((b) => b.slug !== 'repeater')\n\t\tconst fieldsArr = repeaterBlock.fields as Field[]\n\t\tconst validationsIdx = fieldsArr.findIndex(\n\t\t\t(f) => (f as { name?: string }).name === 'validations'\n\t\t)\n\t\tconst subFieldsField: Field = {\n\t\t\tname: 'subFields',\n\t\t\ttype: 'blocks',\n\t\t\tlabel: labelFor(keys.configSubFields),\n\t\t\tblocks: subFieldBlocks,\n\t\t}\n\t\tif (validationsIdx >= 0) {\n\t\t\tfieldsArr.splice(validationsIdx, 0, subFieldsField)\n\t\t} else {\n\t\t\tfieldsArr.push(subFieldsField)\n\t\t}\n\t}\n\n\treturn blocks\n}\n"],"mappings":";;;;;;;;AAYA,MAAa,oBACZ,UACA,cACA,oBACa;CACb,MAAM,iBAAiB,sBAAsB,QAAQ;CACrD,MAAM,SAAkB,CAAC;CACzB,KAAK,MAAM,cAAc,SAAS,OAAO,GAAG;EAC3C,IAAI,aAAsB,WAAW,UAAU,CAAC;EAKhD,IAAI,WAAW,SAAS,aAAa,iBAAiB;GAErD,MAAM,YAAY,WAAW,MAAM,MAAO,EAAwB,SAAS,WAAW;GACtF,MAAM,WAAW,WAAW,MAAM,MAAO,EAAwB,SAAS,UAAU;GACpF,aAAa;IACZ,GAAI,YAAY,CAAC,SAAS,IAAI,CAAC;IAC/B,GAAG,yBAAyB,eAAe;IAC3C,GAAI,WAAW,CAAC,QAAQ,IAAI,CAAC;GAC9B;EACD;EAEA,OAAO,KAAK;GACX,MAAM,WAAW;GACjB,QAAQ;IAAE,UAAU,SAAS,WAAW,KAAK;IAAG,QAAQ,SAAS,WAAW,KAAK;GAAE;GACnF,QAAQ;IACP,GAAG,kBAAkB,cAAc;IACnC,GAAG;IACH;KACC,MAAM;KACN,MAAM;KACN,OAAO,SAAS,KAAK,gBAAgB;KACrC,QAAQ,gBAAgB,cAAc,WAAW,IAAI;IACtD;GACD;EACD,CAAC;CACF;CAKA,MAAM,gBAAgB,OAAO,MAAM,MAAM,EAAE,SAAS,UAAU;CAC9D,IAAI,eAAe;EAClB,MAAM,iBAAiB,OAAO,QAAQ,MAAM,EAAE,SAAS,UAAU;EACjE,MAAM,YAAY,cAAc;EAChC,MAAM,iBAAiB,UAAU,WAC/B,MAAO,EAAwB,SAAS,aAC1C;EACA,MAAM,iBAAwB;GAC7B,MAAM;GACN,MAAM;GACN,OAAO,SAAS,KAAK,eAAe;GACpC,QAAQ;EACT;EACA,IAAI,kBAAkB,GACrB,UAAU,OAAO,gBAAgB,GAAG,cAAc;OAElD,UAAU,KAAK,cAAc;CAE/B;CAEA,OAAO;AACR"}
@@ -4,6 +4,7 @@ import { consentField } from "./consent.js";
4
4
  import { emailField } from "./email.js";
5
5
  import { fileField } from "./file.js";
6
6
  import { numberField } from "./number.js";
7
+ import { repeaterField } from "./repeater.js";
7
8
  import { selectField } from "./select.js";
8
9
  import { textField } from "./text.js";
9
10
  import { textareaField } from "./textarea.js";
@@ -17,7 +18,8 @@ const defaultFieldDefinitions = [
17
18
  checkboxField,
18
19
  calculationField,
19
20
  consentField,
20
- fileField
21
+ fileField,
22
+ repeaterField
21
23
  ];
22
24
  //#endregion
23
25
  export { defaultFieldDefinitions };
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../src/fields/builtin/index.ts"],"sourcesContent":["import type { AnyFormFieldDefinition } from '../types'\nimport { calculationField } from './calculation'\nimport { checkboxField } from './checkbox'\nimport { consentField } from './consent'\nimport { emailField } from './email'\nimport { fileField } from './file'\nimport { numberField } from './number'\nimport { selectField } from './select'\nimport { textField } from './text'\nimport { textareaField } from './textarea'\n\n// Field types are authored with precise value/config generics; the registry stores them erased\n// (config is re-narrowed per matched type at execution). One cast per built-in, no `any`.\nexport const defaultFieldDefinitions: AnyFormFieldDefinition[] = [\n\ttextField as AnyFormFieldDefinition,\n\ttextareaField as AnyFormFieldDefinition,\n\temailField as AnyFormFieldDefinition,\n\tnumberField as AnyFormFieldDefinition,\n\tselectField as AnyFormFieldDefinition,\n\tcheckboxField as AnyFormFieldDefinition,\n\tcalculationField as AnyFormFieldDefinition,\n\tconsentField as AnyFormFieldDefinition,\n\tfileField as AnyFormFieldDefinition,\n]\n"],"mappings":";;;;;;;;;;AAaA,MAAa,0BAAoD;CAChE;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACD"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/fields/builtin/index.ts"],"sourcesContent":["import type { AnyFormFieldDefinition } from '../types'\nimport { calculationField } from './calculation'\nimport { checkboxField } from './checkbox'\nimport { consentField } from './consent'\nimport { emailField } from './email'\nimport { fileField } from './file'\nimport { numberField } from './number'\nimport { repeaterField } from './repeater'\nimport { selectField } from './select'\nimport { textField } from './text'\nimport { textareaField } from './textarea'\n\n// Field types are authored with precise value/config generics; the registry stores them erased\n// (config is re-narrowed per matched type at execution). One cast per built-in, no `any`.\nexport const defaultFieldDefinitions: AnyFormFieldDefinition[] = [\n\ttextField as AnyFormFieldDefinition,\n\ttextareaField as AnyFormFieldDefinition,\n\temailField as AnyFormFieldDefinition,\n\tnumberField as AnyFormFieldDefinition,\n\tselectField as AnyFormFieldDefinition,\n\tcheckboxField as AnyFormFieldDefinition,\n\tcalculationField as AnyFormFieldDefinition,\n\tconsentField as AnyFormFieldDefinition,\n\tfileField as AnyFormFieldDefinition,\n\trepeaterField as AnyFormFieldDefinition,\n]\n"],"mappings":";;;;;;;;;;;AAcA,MAAa,0BAAoD;CAChE;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACD"}
@@ -0,0 +1,44 @@
1
+ import { keys } from "../../translations/keys.js";
2
+ import { labelFor } from "../../translations/server.js";
3
+ import { defineFormField } from "../defineFormField.js";
4
+ //#region src/fields/builtin/repeater.ts
5
+ const repeaterField = defineFormField({
6
+ type: "repeater",
7
+ label: keys.fieldTypeRepeater,
8
+ value: "repeater",
9
+ config: [
10
+ {
11
+ name: "minRows",
12
+ type: "number",
13
+ min: 0,
14
+ label: labelFor(keys.configMinRows)
15
+ },
16
+ {
17
+ name: "maxRows",
18
+ type: "number",
19
+ min: 1,
20
+ label: labelFor(keys.configMaxRows)
21
+ },
22
+ {
23
+ name: "addLabel",
24
+ type: "text",
25
+ label: labelFor(keys.configAddLabel)
26
+ }
27
+ ],
28
+ validate: ({ value, config, t }) => {
29
+ const rows = Array.isArray(value) ? value : [];
30
+ const min = typeof config.minRows === "number" ? config.minRows : 0;
31
+ if (rows.length < min) return t(keys.validationRepeaterMin).replace("{min}", String(min));
32
+ const max = typeof config.maxRows === "number" ? config.maxRows : void 0;
33
+ if (max != null && rows.length > max) return t(keys.validationRepeaterMax).replace("{max}", String(max));
34
+ return true;
35
+ },
36
+ format: ({ value, t }) => {
37
+ const count = Array.isArray(value) ? value.length : 0;
38
+ return t(keys.repeaterRowCount).replace("{count}", String(count));
39
+ }
40
+ });
41
+ //#endregion
42
+ export { repeaterField };
43
+
44
+ //# sourceMappingURL=repeater.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repeater.js","names":[],"sources":["../../../src/fields/builtin/repeater.ts"],"sourcesContent":["import { keys } from '../../translations/keys'\nimport { labelFor } from '../../translations/server'\nimport { defineFormField } from '../defineFormField'\n\ntype RepeaterConfig = {\n\tminRows?: number\n\tmaxRows?: number\n\taddLabel?: string\n}\n\nexport const repeaterField = defineFormField<'repeater', RepeaterConfig>({\n\ttype: 'repeater',\n\tlabel: keys.fieldTypeRepeater,\n\tvalue: 'repeater',\n\tconfig: [\n\t\t{\n\t\t\tname: 'minRows',\n\t\t\ttype: 'number',\n\t\t\tmin: 0,\n\t\t\tlabel: labelFor(keys.configMinRows),\n\t\t},\n\t\t{\n\t\t\tname: 'maxRows',\n\t\t\ttype: 'number',\n\t\t\tmin: 1,\n\t\t\tlabel: labelFor(keys.configMaxRows),\n\t\t},\n\t\t{\n\t\t\tname: 'addLabel',\n\t\t\ttype: 'text',\n\t\t\tlabel: labelFor(keys.configAddLabel),\n\t\t},\n\t\t// subFields blocks field is injected by buildFieldBlocks after the main loop,\n\t\t// so all non-repeater blocks are available and repeater-in-repeater is excluded.\n\t],\n\tvalidate: ({ value, config, t }) => {\n\t\tconst rows = Array.isArray(value) ? value : []\n\t\tconst min = typeof config.minRows === 'number' ? config.minRows : 0\n\t\tif (rows.length < min) {\n\t\t\treturn t(keys.validationRepeaterMin).replace('{min}', String(min))\n\t\t}\n\t\tconst max = typeof config.maxRows === 'number' ? config.maxRows : undefined\n\t\tif (max != null && rows.length > max) {\n\t\t\treturn t(keys.validationRepeaterMax).replace('{max}', String(max))\n\t\t}\n\t\treturn true\n\t},\n\tformat: ({ value, t }) => {\n\t\tconst count = Array.isArray(value) ? value.length : 0\n\t\treturn t(keys.repeaterRowCount).replace('{count}', String(count))\n\t},\n})\n"],"mappings":";;;;AAUA,MAAa,gBAAgB,gBAA4C;CACxE,MAAM;CACN,OAAO,KAAK;CACZ,OAAO;CACP,QAAQ;EACP;GACC,MAAM;GACN,MAAM;GACN,KAAK;GACL,OAAO,SAAS,KAAK,aAAa;EACnC;EACA;GACC,MAAM;GACN,MAAM;GACN,KAAK;GACL,OAAO,SAAS,KAAK,aAAa;EACnC;EACA;GACC,MAAM;GACN,MAAM;GACN,OAAO,SAAS,KAAK,cAAc;EACpC;CAGD;CACA,WAAW,EAAE,OAAO,QAAQ,QAAQ;EACnC,MAAM,OAAO,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC;EAC7C,MAAM,MAAM,OAAO,OAAO,YAAY,WAAW,OAAO,UAAU;EAClE,IAAI,KAAK,SAAS,KACjB,OAAO,EAAE,KAAK,qBAAqB,EAAE,QAAQ,SAAS,OAAO,GAAG,CAAC;EAElE,MAAM,MAAM,OAAO,OAAO,YAAY,WAAW,OAAO,UAAU,KAAA;EAClE,IAAI,OAAO,QAAQ,KAAK,SAAS,KAChC,OAAO,EAAE,KAAK,qBAAqB,EAAE,QAAQ,SAAS,OAAO,GAAG,CAAC;EAElE,OAAO;CACR;CACA,SAAS,EAAE,OAAO,QAAQ;EACzB,MAAM,QAAQ,MAAM,QAAQ,KAAK,IAAI,MAAM,SAAS;EACpD,OAAO,EAAE,KAAK,gBAAgB,EAAE,QAAQ,WAAW,OAAO,KAAK,CAAC;CACjE;AACD,CAAC"}
@@ -5,7 +5,7 @@ import { StandardSchemaV1 } from "@standard-schema/spec";
5
5
 
6
6
  //#region src/fields/types.d.ts
7
7
  /** The stored value shapes a field type can declare. Drives `validate`/`format` typing and value coercion. */
8
- type FormFieldValueKind = 'text' | 'number' | 'boolean' | 'date' | 'text[]' | 'file';
8
+ type FormFieldValueKind = 'text' | 'number' | 'boolean' | 'date' | 'text[]' | 'file' | 'repeater';
9
9
  type ValueKindTypeMap = {
10
10
  text: string;
11
11
  number: number;
@@ -13,6 +13,7 @@ type ValueKindTypeMap = {
13
13
  date: string;
14
14
  'text[]': string[];
15
15
  file: FileRef;
16
+ repeater: Array<Record<string, unknown>>;
16
17
  };
17
18
  type ValueOfKind<K extends FormFieldValueKind> = ValueKindTypeMap[K];
18
19
  /** Resolved per-instance config values for a field type (loose at the DB boundary, narrowed by the author's generic). */
package/dist/index.d.ts CHANGED
@@ -19,6 +19,7 @@ import { ActionOption, ActionRegistry, ActionsConfig, resolveActions } from "./a
19
19
  import { ActionResult } from "./actions/runActions.js";
20
20
  import { AggregateFieldResponsesArgs, AggregateFormResponsesArgs, aggregateFieldResponses, aggregateFormResponses, fieldHasOptions } from "./aggregation/aggregateResponses.js";
21
21
  import { ResolveResultsRequestArgs, ResolveResultsRequestResult, resolveFormResultsRequest } from "./aggregation/resolveResultsRequest.js";
22
+ import { CollectionOverrides } from "./plugin/collectionOverrides.js";
22
23
  import { FORM_UPLOADS_SLUG, UploadsCollectionConfig, UploadsOption, buildUploadsCollection, resolveUploads } from "./collections/uploads.js";
23
24
  import { FieldCondition } from "./conditions/types.js";
24
25
  import { AnyConsentSource, ConsentLink, ConsentResolveArgs, ConsentResolved, ConsentSource, defineConsentSource } from "./consent/defineConsentSource.js";
@@ -62,6 +63,24 @@ type FormBuilderPluginOptions = {
62
63
  consentSources?: ConsentSourcesConfig; /** The built-in `form-uploads` collection backing file fields. `false` disables it (bring your own); an object overrides slug/upload/access/fields. */
63
64
  uploads?: UploadsOption; /** Honeypot + rate-limiting (on by default) + a captcha adapter seam + upload-ownership scoping. `false` disables the whole subsystem. */
64
65
  spam?: SpamOption;
66
+ /**
67
+ * When `true`, the raw `values`, `descriptors`, and `consent` JSON fields are visible in the
68
+ * submission admin view. Default `false` — those fields are fully represented by the
69
+ * `SubmissionAnswers` UI component and are noisy when shown alongside it.
70
+ */
71
+ showSubmissionRawFields?: boolean;
72
+ /**
73
+ * Override individual plugin-managed collections using explicit spreads. Each key accepts a
74
+ * `CollectionOverrides` object: top-level keys are spread with the plugin's defaults (spread
75
+ * order per key determines who wins), hooks are appended after the plugin's own hooks, and
76
+ * `fields` is a function that receives the default fields and returns the final array so
77
+ * additions/removals are always intentional.
78
+ */
79
+ overrides?: {
80
+ forms?: CollectionOverrides;
81
+ formSubmissions?: CollectionOverrides;
82
+ uploads?: CollectionOverrides;
83
+ };
65
84
  };
66
85
  declare module 'payload' {
67
86
  interface RegisteredPlugins {
package/dist/index.js CHANGED
@@ -61,7 +61,9 @@ const formBuilder = definePlugin({
61
61
  hasJobsPlugin: Boolean(plugins["@10x-media/jobs"]),
62
62
  events: options.events,
63
63
  uploads,
64
- spam
64
+ spam,
65
+ showSubmissionRawFields: options.showSubmissionRawFields ?? false,
66
+ overrides: options.overrides
65
67
  });
66
68
  return config;
67
69
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import { type Config, definePlugin } from 'payload'\nimport { defaultActionDefinitions } from './actions/builtin'\nimport type { ActionsConfig } from './actions/registry'\nimport { resolveActions } from './actions/registry'\nimport { resolveUploads, type UploadsOption } from './collections/uploads'\nimport { defaultConsentSources } from './consent/builtin'\nimport type { ConsentSourcesConfig } from './consent/registry'\nimport { resolveConsentSources } from './consent/registry'\nimport type { FormEventSink } from './events/types'\nimport { defaultFieldDefinitions } from './fields/builtin'\nimport { type FieldTypesConfig, resolveFieldTypes } from './fields/registry'\nimport { registerCollections } from './plugin/registerCollections'\nimport { registerTranslations } from './plugin/registerTranslations'\nimport { defaultPresentationDescriptors } from './presentations/defaults'\nimport type { PresentationsDescriptorConfig } from './presentations/registry'\nimport { resolvePresentationDescriptors } from './presentations/registry'\nimport { resolveSpamConfig } from './spam/resolveSpam'\nimport type { SpamOption } from './spam/types'\nimport type { TranslationsOption } from './translations'\nimport { defaultValidationRules } from './validation/builtin'\nimport { resolveValidationRules, type ValidationRulesConfig } from './validation/registry'\n\nexport type FormBuilderPluginOptions = {\n\tdisabled?: boolean\n\t/**\n\t * Per-locale overrides for this plugin's UI strings, keyed by the typed\n\t * translation keys exported from `@10x-media/form-builder/i18n`. Values win\n\t * over the built-in locales key-by-key; locales the plugin does not ship are\n\t * added whole. App-level `i18n.translations` still wins over both.\n\t */\n\ttranslations?: TranslationsOption\n\t/** Pluggable sink for form lifecycle events. Defaults to a no-op; analytics adapters or a future analytics plugin subscribe here. */\n\tevents?: FormEventSink\n\t/** Add, override, or remove field types. `false` removes a built-in, `true` keeps it, an object adds or replaces one. */\n\tfields?: FieldTypesConfig\n\t/** Add, override, or remove presentations. `false` removes a built-in, `true` keeps it, an object adds or replaces one. */\n\tpresentations?: PresentationsDescriptorConfig\n\t/** Add, override, or remove validation rule types. `false` removes a built-in, `true` keeps it, an object adds or replaces one. */\n\trules?: ValidationRulesConfig\n\t/** Add, override, or remove post-submit action types. `false` removes a built-in, `true` keeps it, an object adds or replaces one. */\n\tactions?: ActionsConfig\n\t/** Add, override, or remove consent source types. `false` removes a built-in, `true` keeps it, an object adds or replaces one. */\n\tconsentSources?: ConsentSourcesConfig\n\t/** The built-in `form-uploads` collection backing file fields. `false` disables it (bring your own); an object overrides slug/upload/access/fields. */\n\tuploads?: UploadsOption\n\t/** Honeypot + rate-limiting (on by default) + a captcha adapter seam + upload-ownership scoping. `false` disables the whole subsystem. */\n\tspam?: SpamOption\n}\n\ndeclare module 'payload' {\n\tinterface RegisteredPlugins {\n\t\t'@10x-media/form-builder': FormBuilderPluginOptions\n\t}\n}\n\nexport const formBuilder = definePlugin<FormBuilderPluginOptions>({\n\tslug: '@10x-media/form-builder',\n\torder: 50,\n\tplugin: ({ config, plugins, ...options }): Config => {\n\t\tif (options.disabled === true) {\n\t\t\treturn config\n\t\t}\n\t\tconst registry = resolveFieldTypes(defaultFieldDefinitions, options.fields)\n\t\tconst ruleRegistry = resolveValidationRules(defaultValidationRules, options.rules)\n\t\tconst consentRegistry = resolveConsentSources(defaultConsentSources, options.consentSources)\n\t\tconst presentationRegistry = resolvePresentationDescriptors(\n\t\t\tdefaultPresentationDescriptors,\n\t\t\toptions.presentations\n\t\t)\n\t\tconst actionRegistry = resolveActions(defaultActionDefinitions, options.actions)\n\t\tconst uploads = resolveUploads(options.uploads)\n\t\tconst spam = resolveSpamConfig(options.spam)\n\t\tregisterTranslations(config, options.translations)\n\t\tregisterCollections({\n\t\t\tconfig,\n\t\t\tregistry,\n\t\t\truleRegistry,\n\t\t\tconsentRegistry,\n\t\t\tpresentationRegistry,\n\t\t\tactionRegistry,\n\t\t\thasJobsPlugin: Boolean(plugins['@10x-media/jobs']),\n\t\t\tevents: options.events,\n\t\t\tuploads,\n\t\t\tspam,\n\t\t})\n\t\treturn config\n\t},\n})\n\nexport { defaultActionDefinitions } from './actions/builtin'\nexport type { ActionDefinition, ActionRunArgs, AnyActionDefinition } from './actions/defineAction'\nexport { defineAction } from './actions/defineAction'\nexport type { ActionOption, ActionRegistry, ActionsConfig } from './actions/registry'\nexport { resolveActions } from './actions/registry'\nexport type { ActionResult } from './actions/runActions'\nexport { SIGNATURE_HEADER, signPayload } from './actions/sign'\nexport type {\n\tAggregateFieldResponsesArgs,\n\tAggregateFormResponsesArgs,\n} from './aggregation/aggregateResponses'\nexport {\n\taggregateFieldResponses,\n\taggregateFormResponses,\n\tfieldHasOptions,\n} from './aggregation/aggregateResponses'\nexport { aggregateRowForField, aggregateRowsForFields } from './aggregation/aggregateRows'\nexport type {\n\tResolveResultsRequestArgs,\n\tResolveResultsRequestResult,\n} from './aggregation/resolveResultsRequest'\nexport { resolveFormResultsRequest } from './aggregation/resolveResultsRequest'\nexport type {\n\tAggregationBucket,\n\tAggregationRow,\n\tFieldAggregation,\n\tFieldMeta,\n\tSubmissionStatusFilter,\n} from './aggregation/types'\nexport { calcExpressionOf, computeCalcFields } from './calc/computeCalcFields'\nexport { evaluateCalc } from './calc/evaluate'\nexport { normalizeCalc } from './calc/normalizeCalc'\nexport type { CalcExpression } from './calc/types'\nexport type { UploadsCollectionConfig, UploadsOption } from './collections/uploads'\nexport { buildUploadsCollection, FORM_UPLOADS_SLUG, resolveUploads } from './collections/uploads'\nexport { evaluateCondition } from './conditions/evaluate'\nexport type { FieldCondition } from './conditions/types'\nexport { defaultConsentSources } from './consent/builtin'\nexport type { ConsentProof } from './consent/captureConsent'\nexport { captureConsent } from './consent/captureConsent'\nexport type {\n\tAnyConsentSource,\n\tConsentLink,\n\tConsentResolveArgs,\n\tConsentResolved,\n\tConsentSource,\n} from './consent/defineConsentSource'\nexport { defineConsentSource } from './consent/defineConsentSource'\nexport type {\n\tConsentSourceOption,\n\tConsentSourceRegistry,\n\tConsentSourcesConfig,\n} from './consent/registry'\nexport { resolveConsentSources } from './consent/registry'\nexport { resolveConsentLinks } from './consent/resolveConsentLinks'\nexport { resolvePublishedVersionRef } from './consent/resolvePublishedVersionRef'\nexport { defineFormField } from './fields/defineFormField'\nexport type { FieldTypeOption, FieldTypeRegistry, FieldTypesConfig } from './fields/registry'\nexport type {\n\tAnyFormFieldDefinition,\n\tFormFieldDefinition,\n\tFormFieldFormat,\n\tFormFieldValidate,\n\tFormFieldValueKind,\n} from './fields/types'\nexport type { PrefillOptions } from './prefill/valuesFromSearchParams'\nexport { valuesFromSearchParams } from './prefill/valuesFromSearchParams'\nexport { DEFAULT_PRESENTATION_NAME, defaultPresentationDescriptors } from './presentations/defaults'\nexport type {\n\tPresentationDescriptorOption,\n\tPresentationDescriptorRegistry,\n\tPresentationsDescriptorConfig,\n} from './presentations/registry'\nexport { resolvePresentationDescriptors } from './presentations/registry'\nexport type {\n\tPresentationDensity,\n\tPresentationDescriptor,\n\tPresentationSurface,\n} from './presentations/types'\nexport { interpolate } from './recall/interpolate'\nexport type { RecallResolver } from './recall/resolver'\nexport { buildRecallResolver, optionLabelsFor } from './recall/resolver'\nexport { defineCaptchaProvider } from './spam/captcha'\nexport { CAPTCHA_TOKEN_KEY, DEFAULT_HONEYPOT_FIELD } from './spam/constants'\nexport { defaultIdentify } from './spam/identify'\nexport { createKvRateLimiter } from './spam/rateLimiter'\nexport { resolveSpamConfig } from './spam/resolveSpam'\nexport type {\n\tCaptchaProvider,\n\tCaptchaVerifyArgs,\n\tIdentifyFn,\n\tRateLimitCheckArgs,\n\tRateLimitConfig,\n\tRateLimiter,\n\tRateLimitResult,\n\tSpamConfig,\n\tSpamMetadataConfig,\n\tSpamOption,\n} from './spam/types'\nexport { captureFileRef } from './uploads/captureFileRef'\nexport { resolveFileRef } from './uploads/resolveFileRef'\nexport type { FileFieldConfig, FileRef, FileRefError } from './uploads/types'\nexport { defineValidationRule } from './validation/defineValidationRule'\nexport type {\n\tValidationRuleOption,\n\tValidationRuleRegistry,\n\tValidationRulesConfig,\n} from './validation/registry'\nexport type {\n\tAnyValidationRuleDefinition,\n\tValidationRuleDefinition,\n\tValidationRuleResult,\n\tValidationSeverity,\n} from './validation/types'\nexport type { FormBuilderPluginOptions as PluginOptions }\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuDA,MAAa,cAAc,aAAuC;CACjE,MAAM;CACN,OAAO;CACP,SAAS,EAAE,QAAQ,SAAS,GAAG,cAAsB;EACpD,IAAI,QAAQ,aAAa,MACxB,OAAO;EAER,MAAM,WAAW,kBAAkB,yBAAyB,QAAQ,MAAM;EAC1E,MAAM,eAAe,uBAAuB,wBAAwB,QAAQ,KAAK;EACjF,MAAM,kBAAkB,sBAAsB,uBAAuB,QAAQ,cAAc;EAC3F,MAAM,uBAAuB,+BAC5B,gCACA,QAAQ,aACT;EACA,MAAM,iBAAiB,eAAe,0BAA0B,QAAQ,OAAO;EAC/E,MAAM,UAAU,eAAe,QAAQ,OAAO;EAC9C,MAAM,OAAO,kBAAkB,QAAQ,IAAI;EAC3C,qBAAqB,QAAQ,QAAQ,YAAY;EACjD,oBAAoB;GACnB;GACA;GACA;GACA;GACA;GACA;GACA,eAAe,QAAQ,QAAQ,kBAAkB;GACjD,QAAQ,QAAQ;GAChB;GACA;EACD,CAAC;EACD,OAAO;CACR;AACD,CAAC"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import { type Config, definePlugin } from 'payload'\nimport { defaultActionDefinitions } from './actions/builtin'\nimport type { ActionsConfig } from './actions/registry'\nimport { resolveActions } from './actions/registry'\nimport { resolveUploads, type UploadsOption } from './collections/uploads'\nimport { defaultConsentSources } from './consent/builtin'\nimport type { ConsentSourcesConfig } from './consent/registry'\nimport { resolveConsentSources } from './consent/registry'\nimport type { FormEventSink } from './events/types'\nimport { defaultFieldDefinitions } from './fields/builtin'\nimport { type FieldTypesConfig, resolveFieldTypes } from './fields/registry'\nimport type { CollectionOverrides } from './plugin/collectionOverrides'\nimport { registerCollections } from './plugin/registerCollections'\nimport { registerTranslations } from './plugin/registerTranslations'\nimport { defaultPresentationDescriptors } from './presentations/defaults'\nimport type { PresentationsDescriptorConfig } from './presentations/registry'\nimport { resolvePresentationDescriptors } from './presentations/registry'\nimport { resolveSpamConfig } from './spam/resolveSpam'\nimport type { SpamOption } from './spam/types'\nimport type { TranslationsOption } from './translations'\nimport { defaultValidationRules } from './validation/builtin'\nimport { resolveValidationRules, type ValidationRulesConfig } from './validation/registry'\n\nexport type FormBuilderPluginOptions = {\n\tdisabled?: boolean\n\t/**\n\t * Per-locale overrides for this plugin's UI strings, keyed by the typed\n\t * translation keys exported from `@10x-media/form-builder/i18n`. Values win\n\t * over the built-in locales key-by-key; locales the plugin does not ship are\n\t * added whole. App-level `i18n.translations` still wins over both.\n\t */\n\ttranslations?: TranslationsOption\n\t/** Pluggable sink for form lifecycle events. Defaults to a no-op; analytics adapters or a future analytics plugin subscribe here. */\n\tevents?: FormEventSink\n\t/** Add, override, or remove field types. `false` removes a built-in, `true` keeps it, an object adds or replaces one. */\n\tfields?: FieldTypesConfig\n\t/** Add, override, or remove presentations. `false` removes a built-in, `true` keeps it, an object adds or replaces one. */\n\tpresentations?: PresentationsDescriptorConfig\n\t/** Add, override, or remove validation rule types. `false` removes a built-in, `true` keeps it, an object adds or replaces one. */\n\trules?: ValidationRulesConfig\n\t/** Add, override, or remove post-submit action types. `false` removes a built-in, `true` keeps it, an object adds or replaces one. */\n\tactions?: ActionsConfig\n\t/** Add, override, or remove consent source types. `false` removes a built-in, `true` keeps it, an object adds or replaces one. */\n\tconsentSources?: ConsentSourcesConfig\n\t/** The built-in `form-uploads` collection backing file fields. `false` disables it (bring your own); an object overrides slug/upload/access/fields. */\n\tuploads?: UploadsOption\n\t/** Honeypot + rate-limiting (on by default) + a captcha adapter seam + upload-ownership scoping. `false` disables the whole subsystem. */\n\tspam?: SpamOption\n\t/**\n\t * When `true`, the raw `values`, `descriptors`, and `consent` JSON fields are visible in the\n\t * submission admin view. Default `false` — those fields are fully represented by the\n\t * `SubmissionAnswers` UI component and are noisy when shown alongside it.\n\t */\n\tshowSubmissionRawFields?: boolean\n\t/**\n\t * Override individual plugin-managed collections using explicit spreads. Each key accepts a\n\t * `CollectionOverrides` object: top-level keys are spread with the plugin's defaults (spread\n\t * order per key determines who wins), hooks are appended after the plugin's own hooks, and\n\t * `fields` is a function that receives the default fields and returns the final array so\n\t * additions/removals are always intentional.\n\t */\n\toverrides?: {\n\t\tforms?: CollectionOverrides\n\t\tformSubmissions?: CollectionOverrides\n\t\tuploads?: CollectionOverrides\n\t}\n}\n\ndeclare module 'payload' {\n\tinterface RegisteredPlugins {\n\t\t'@10x-media/form-builder': FormBuilderPluginOptions\n\t}\n}\n\nexport const formBuilder = definePlugin<FormBuilderPluginOptions>({\n\tslug: '@10x-media/form-builder',\n\torder: 50,\n\tplugin: ({ config, plugins, ...options }): Config => {\n\t\tif (options.disabled === true) {\n\t\t\treturn config\n\t\t}\n\t\tconst registry = resolveFieldTypes(defaultFieldDefinitions, options.fields)\n\t\tconst ruleRegistry = resolveValidationRules(defaultValidationRules, options.rules)\n\t\tconst consentRegistry = resolveConsentSources(defaultConsentSources, options.consentSources)\n\t\tconst presentationRegistry = resolvePresentationDescriptors(\n\t\t\tdefaultPresentationDescriptors,\n\t\t\toptions.presentations\n\t\t)\n\t\tconst actionRegistry = resolveActions(defaultActionDefinitions, options.actions)\n\t\tconst uploads = resolveUploads(options.uploads)\n\t\tconst spam = resolveSpamConfig(options.spam)\n\t\tregisterTranslations(config, options.translations)\n\t\tregisterCollections({\n\t\t\tconfig,\n\t\t\tregistry,\n\t\t\truleRegistry,\n\t\t\tconsentRegistry,\n\t\t\tpresentationRegistry,\n\t\t\tactionRegistry,\n\t\t\thasJobsPlugin: Boolean(plugins['@10x-media/jobs']),\n\t\t\tevents: options.events,\n\t\t\tuploads,\n\t\t\tspam,\n\t\t\tshowSubmissionRawFields: options.showSubmissionRawFields ?? false,\n\t\t\toverrides: options.overrides,\n\t\t})\n\t\treturn config\n\t},\n})\n\nexport { defaultActionDefinitions } from './actions/builtin'\nexport type { ActionDefinition, ActionRunArgs, AnyActionDefinition } from './actions/defineAction'\nexport { defineAction } from './actions/defineAction'\nexport type { ActionOption, ActionRegistry, ActionsConfig } from './actions/registry'\nexport { resolveActions } from './actions/registry'\nexport type { ActionResult } from './actions/runActions'\nexport { SIGNATURE_HEADER, signPayload } from './actions/sign'\nexport type {\n\tAggregateFieldResponsesArgs,\n\tAggregateFormResponsesArgs,\n} from './aggregation/aggregateResponses'\nexport {\n\taggregateFieldResponses,\n\taggregateFormResponses,\n\tfieldHasOptions,\n} from './aggregation/aggregateResponses'\nexport { aggregateRowForField, aggregateRowsForFields } from './aggregation/aggregateRows'\nexport type {\n\tResolveResultsRequestArgs,\n\tResolveResultsRequestResult,\n} from './aggregation/resolveResultsRequest'\nexport { resolveFormResultsRequest } from './aggregation/resolveResultsRequest'\nexport type {\n\tAggregationBucket,\n\tAggregationRow,\n\tFieldAggregation,\n\tFieldMeta,\n\tSubmissionStatusFilter,\n} from './aggregation/types'\nexport { calcExpressionOf, computeCalcFields } from './calc/computeCalcFields'\nexport { evaluateCalc } from './calc/evaluate'\nexport { normalizeCalc } from './calc/normalizeCalc'\nexport type { CalcExpression } from './calc/types'\nexport type { UploadsCollectionConfig, UploadsOption } from './collections/uploads'\nexport { buildUploadsCollection, FORM_UPLOADS_SLUG, resolveUploads } from './collections/uploads'\nexport { evaluateCondition } from './conditions/evaluate'\nexport type { FieldCondition } from './conditions/types'\nexport { defaultConsentSources } from './consent/builtin'\nexport type { ConsentProof } from './consent/captureConsent'\nexport { captureConsent } from './consent/captureConsent'\nexport type {\n\tAnyConsentSource,\n\tConsentLink,\n\tConsentResolveArgs,\n\tConsentResolved,\n\tConsentSource,\n} from './consent/defineConsentSource'\nexport { defineConsentSource } from './consent/defineConsentSource'\nexport type {\n\tConsentSourceOption,\n\tConsentSourceRegistry,\n\tConsentSourcesConfig,\n} from './consent/registry'\nexport { resolveConsentSources } from './consent/registry'\nexport { resolveConsentLinks } from './consent/resolveConsentLinks'\nexport { resolvePublishedVersionRef } from './consent/resolvePublishedVersionRef'\nexport { defineFormField } from './fields/defineFormField'\nexport type { FieldTypeOption, FieldTypeRegistry, FieldTypesConfig } from './fields/registry'\nexport type {\n\tAnyFormFieldDefinition,\n\tFormFieldDefinition,\n\tFormFieldFormat,\n\tFormFieldValidate,\n\tFormFieldValueKind,\n} from './fields/types'\nexport type { PrefillOptions } from './prefill/valuesFromSearchParams'\nexport { valuesFromSearchParams } from './prefill/valuesFromSearchParams'\nexport { DEFAULT_PRESENTATION_NAME, defaultPresentationDescriptors } from './presentations/defaults'\nexport type {\n\tPresentationDescriptorOption,\n\tPresentationDescriptorRegistry,\n\tPresentationsDescriptorConfig,\n} from './presentations/registry'\nexport { resolvePresentationDescriptors } from './presentations/registry'\nexport type {\n\tPresentationDensity,\n\tPresentationDescriptor,\n\tPresentationSurface,\n} from './presentations/types'\nexport { interpolate } from './recall/interpolate'\nexport type { RecallResolver } from './recall/resolver'\nexport { buildRecallResolver, optionLabelsFor } from './recall/resolver'\nexport { defineCaptchaProvider } from './spam/captcha'\nexport { CAPTCHA_TOKEN_KEY, DEFAULT_HONEYPOT_FIELD } from './spam/constants'\nexport { defaultIdentify } from './spam/identify'\nexport { createKvRateLimiter } from './spam/rateLimiter'\nexport { resolveSpamConfig } from './spam/resolveSpam'\nexport type {\n\tCaptchaProvider,\n\tCaptchaVerifyArgs,\n\tIdentifyFn,\n\tRateLimitCheckArgs,\n\tRateLimitConfig,\n\tRateLimiter,\n\tRateLimitResult,\n\tSpamConfig,\n\tSpamMetadataConfig,\n\tSpamOption,\n} from './spam/types'\nexport { captureFileRef } from './uploads/captureFileRef'\nexport { resolveFileRef } from './uploads/resolveFileRef'\nexport type { FileFieldConfig, FileRef, FileRefError } from './uploads/types'\nexport { defineValidationRule } from './validation/defineValidationRule'\nexport type {\n\tValidationRuleOption,\n\tValidationRuleRegistry,\n\tValidationRulesConfig,\n} from './validation/registry'\nexport type {\n\tAnyValidationRuleDefinition,\n\tValidationRuleDefinition,\n\tValidationRuleResult,\n\tValidationSeverity,\n} from './validation/types'\nexport type { FormBuilderPluginOptions as PluginOptions }\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0EA,MAAa,cAAc,aAAuC;CACjE,MAAM;CACN,OAAO;CACP,SAAS,EAAE,QAAQ,SAAS,GAAG,cAAsB;EACpD,IAAI,QAAQ,aAAa,MACxB,OAAO;EAER,MAAM,WAAW,kBAAkB,yBAAyB,QAAQ,MAAM;EAC1E,MAAM,eAAe,uBAAuB,wBAAwB,QAAQ,KAAK;EACjF,MAAM,kBAAkB,sBAAsB,uBAAuB,QAAQ,cAAc;EAC3F,MAAM,uBAAuB,+BAC5B,gCACA,QAAQ,aACT;EACA,MAAM,iBAAiB,eAAe,0BAA0B,QAAQ,OAAO;EAC/E,MAAM,UAAU,eAAe,QAAQ,OAAO;EAC9C,MAAM,OAAO,kBAAkB,QAAQ,IAAI;EAC3C,qBAAqB,QAAQ,QAAQ,YAAY;EACjD,oBAAoB;GACnB;GACA;GACA;GACA;GACA;GACA;GACA,eAAe,QAAQ,QAAQ,kBAAkB;GACjD,QAAQ,QAAQ;GAChB;GACA;GACA,yBAAyB,QAAQ,2BAA2B;GAC5D,WAAW,QAAQ;EACpB,CAAC;EACD,OAAO;CACR;AACD,CAAC"}
@@ -0,0 +1,22 @@
1
+ import { CollectionConfig, Field } from "payload";
2
+
3
+ //#region src/plugin/collectionOverrides.d.ts
4
+ /**
5
+ * Instead of accepting a static field array (which deepMerge would silently replace), the consumer
6
+ * receives the plugin's default fields and returns the final array. This makes the intent explicit:
7
+ * append, prepend, filter out, or wrap any field without risk of accidentally wiping the defaults.
8
+ */
9
+ type FieldsOverride = (args: {
10
+ defaultFields: Field[];
11
+ }) => Field[];
12
+ /**
13
+ * Typed override surface for plugin-managed collections. Top-level keys use explicit spread order
14
+ * (see each collection builder) so the consumer always knows which values they can override and
15
+ * which are locked by the plugin for correctness or security.
16
+ */
17
+ type CollectionOverrides = {
18
+ fields?: FieldsOverride;
19
+ } & Partial<Omit<CollectionConfig, 'fields'>>;
20
+ //#endregion
21
+ export { CollectionOverrides, FieldsOverride };
22
+ //# sourceMappingURL=collectionOverrides.d.ts.map
@@ -3,24 +3,50 @@ import { buildSubmissionsCollection } from "../collections/formSubmissions.js";
3
3
  import { registerActionsTask } from "../actions/task.js";
4
4
  import { buildUploadOwnerStamp, buildUploadRateLimit } from "../spam/uploadHooks.js";
5
5
  //#region src/plugin/registerCollections.ts
6
- const registerCollections = ({ config, registry, ruleRegistry, consentRegistry, presentationRegistry, actionRegistry, hasJobsPlugin, events, uploads, spam }) => {
6
+ const registerCollections = ({ config, registry, ruleRegistry, consentRegistry, presentationRegistry, actionRegistry, hasJobsPlugin, events, uploads, spam, showSubmissionRawFields, overrides }) => {
7
7
  registerActionsTask(config, actionRegistry);
8
8
  const hasRunner = Boolean(config.jobs?.autoRun) || hasJobsPlugin;
9
- if (spam && uploads.enabled && uploads.collection) {
10
- const collection = uploads.collection;
11
- collection.hooks = collection.hooks ?? {};
12
- collection.hooks.beforeOperation = [...collection.hooks.beforeOperation ?? [], buildUploadRateLimit(spam)];
13
- collection.hooks.beforeValidate = [...collection.hooks.beforeValidate ?? [], buildUploadOwnerStamp(spam)];
9
+ let uploadsCollection = null;
10
+ if (uploads.enabled && uploads.collection) {
11
+ const col = uploads.collection;
12
+ if (spam) {
13
+ col.hooks = col.hooks ?? {};
14
+ col.hooks.beforeOperation = [...col.hooks.beforeOperation ?? [], buildUploadRateLimit(spam)];
15
+ col.hooks.beforeValidate = [...col.hooks.beforeValidate ?? [], buildUploadOwnerStamp(spam)];
16
+ }
17
+ const ov = overrides?.uploads;
18
+ if (ov) {
19
+ const defaultFields = col.fields ?? [];
20
+ uploadsCollection = {
21
+ ...col,
22
+ ...ov ?? {},
23
+ access: {
24
+ ...col.access ?? {},
25
+ ...ov.access ?? {}
26
+ },
27
+ admin: {
28
+ ...col.admin ?? {},
29
+ ...ov.admin ?? {}
30
+ },
31
+ hooks: {
32
+ ...ov.hooks ?? {},
33
+ beforeOperation: [...col.hooks?.beforeOperation ?? [], ...ov.hooks?.beforeOperation ?? []],
34
+ beforeValidate: [...col.hooks?.beforeValidate ?? [], ...ov.hooks?.beforeValidate ?? []]
35
+ },
36
+ fields: ov.fields ? ov.fields({ defaultFields }) : defaultFields
37
+ };
38
+ } else uploadsCollection = col;
14
39
  }
15
40
  config.collections = [
16
41
  ...config.collections ?? [],
17
- ...uploads.enabled && uploads.collection ? [uploads.collection] : [],
42
+ ...uploadsCollection ? [uploadsCollection] : [],
18
43
  buildFormsCollection({
19
44
  registry,
20
45
  ruleRegistry,
21
46
  consentRegistry,
22
47
  presentationRegistry,
23
- actionRegistry
48
+ actionRegistry,
49
+ overrides: overrides?.forms
24
50
  }),
25
51
  buildSubmissionsCollection({
26
52
  registry,
@@ -30,7 +56,9 @@ const registerCollections = ({ config, registry, ruleRegistry, consentRegistry,
30
56
  events,
31
57
  hasRunner,
32
58
  uploadSlug: uploads.slug,
33
- spam
59
+ spam,
60
+ showRawFields: showSubmissionRawFields,
61
+ overrides: overrides?.formSubmissions
34
62
  })
35
63
  ];
36
64
  };
@@ -1 +1 @@
1
- {"version":3,"file":"registerCollections.js","names":[],"sources":["../../src/plugin/registerCollections.ts"],"sourcesContent":["import type { CollectionConfig, Config } from 'payload'\nimport type { ActionRegistry } from '../actions/registry'\nimport { registerActionsTask } from '../actions/task'\nimport { buildSubmissionsCollection } from '../collections/formSubmissions'\nimport { buildFormsCollection } from '../collections/forms'\nimport type { ConsentSourceRegistry } from '../consent/registry'\nimport type { FormEventSink } from '../events/types'\nimport type { FieldTypeRegistry } from '../fields/registry'\nimport type { PresentationDescriptorRegistry } from '../presentations/registry'\nimport type { ResolvedSpamConfig } from '../spam/types'\nimport { buildUploadOwnerStamp, buildUploadRateLimit } from '../spam/uploadHooks'\nimport type { ValidationRuleRegistry } from '../validation/registry'\n\ntype RegisterCollectionsArgs = {\n\tconfig: Config\n\tregistry: FieldTypeRegistry\n\truleRegistry: ValidationRuleRegistry\n\tconsentRegistry: ConsentSourceRegistry\n\tpresentationRegistry: PresentationDescriptorRegistry\n\tactionRegistry: ActionRegistry\n\thasJobsPlugin: boolean\n\tevents?: FormEventSink\n\tuploads: { enabled: boolean; slug: string; collection?: CollectionConfig }\n\tspam: ResolvedSpamConfig | false\n}\n\nexport const registerCollections = ({\n\tconfig,\n\tregistry,\n\truleRegistry,\n\tconsentRegistry,\n\tpresentationRegistry,\n\tactionRegistry,\n\thasJobsPlugin,\n\tevents,\n\tuploads,\n\tspam,\n}: RegisterCollectionsArgs): void => {\n\tregisterActionsTask(config, actionRegistry)\n\tconst hasRunner = Boolean(config.jobs?.autoRun) || hasJobsPlugin\n\tif (spam && uploads.enabled && uploads.collection) {\n\t\tconst collection = uploads.collection\n\t\tcollection.hooks = collection.hooks ?? {}\n\t\tcollection.hooks.beforeOperation = [\n\t\t\t...(collection.hooks.beforeOperation ?? []),\n\t\t\tbuildUploadRateLimit(spam),\n\t\t]\n\t\tcollection.hooks.beforeValidate = [\n\t\t\t...(collection.hooks.beforeValidate ?? []),\n\t\t\tbuildUploadOwnerStamp(spam),\n\t\t]\n\t}\n\tconfig.collections = [\n\t\t...(config.collections ?? []),\n\t\t...(uploads.enabled && uploads.collection ? [uploads.collection] : []),\n\t\tbuildFormsCollection({\n\t\t\tregistry,\n\t\t\truleRegistry,\n\t\t\tconsentRegistry,\n\t\t\tpresentationRegistry,\n\t\t\tactionRegistry,\n\t\t}),\n\t\tbuildSubmissionsCollection({\n\t\t\tregistry,\n\t\t\truleRegistry,\n\t\t\tconsentRegistry,\n\t\t\tactionRegistry,\n\t\t\tevents,\n\t\t\thasRunner,\n\t\t\tuploadSlug: uploads.slug,\n\t\t\tspam,\n\t\t}),\n\t]\n}\n"],"mappings":";;;;;AA0BA,MAAa,uBAAuB,EACnC,QACA,UACA,cACA,iBACA,sBACA,gBACA,eACA,QACA,SACA,WACoC;CACpC,oBAAoB,QAAQ,cAAc;CAC1C,MAAM,YAAY,QAAQ,OAAO,MAAM,OAAO,KAAK;CACnD,IAAI,QAAQ,QAAQ,WAAW,QAAQ,YAAY;EAClD,MAAM,aAAa,QAAQ;EAC3B,WAAW,QAAQ,WAAW,SAAS,CAAC;EACxC,WAAW,MAAM,kBAAkB,CAClC,GAAI,WAAW,MAAM,mBAAmB,CAAC,GACzC,qBAAqB,IAAI,CAC1B;EACA,WAAW,MAAM,iBAAiB,CACjC,GAAI,WAAW,MAAM,kBAAkB,CAAC,GACxC,sBAAsB,IAAI,CAC3B;CACD;CACA,OAAO,cAAc;EACpB,GAAI,OAAO,eAAe,CAAC;EAC3B,GAAI,QAAQ,WAAW,QAAQ,aAAa,CAAC,QAAQ,UAAU,IAAI,CAAC;EACpE,qBAAqB;GACpB;GACA;GACA;GACA;GACA;EACD,CAAC;EACD,2BAA2B;GAC1B;GACA;GACA;GACA;GACA;GACA;GACA,YAAY,QAAQ;GACpB;EACD,CAAC;CACF;AACD"}
1
+ {"version":3,"file":"registerCollections.js","names":[],"sources":["../../src/plugin/registerCollections.ts"],"sourcesContent":["import type { CollectionConfig, Config, Field } from 'payload'\nimport type { ActionRegistry } from '../actions/registry'\nimport { registerActionsTask } from '../actions/task'\nimport { buildSubmissionsCollection } from '../collections/formSubmissions'\nimport { buildFormsCollection } from '../collections/forms'\nimport type { ConsentSourceRegistry } from '../consent/registry'\nimport type { FormEventSink } from '../events/types'\nimport type { FieldTypeRegistry } from '../fields/registry'\nimport type { PresentationDescriptorRegistry } from '../presentations/registry'\nimport type { ResolvedSpamConfig } from '../spam/types'\nimport { buildUploadOwnerStamp, buildUploadRateLimit } from '../spam/uploadHooks'\nimport type { ValidationRuleRegistry } from '../validation/registry'\nimport type { CollectionOverrides } from './collectionOverrides'\n\ntype RegisterCollectionsArgs = {\n\tconfig: Config\n\tregistry: FieldTypeRegistry\n\truleRegistry: ValidationRuleRegistry\n\tconsentRegistry: ConsentSourceRegistry\n\tpresentationRegistry: PresentationDescriptorRegistry\n\tactionRegistry: ActionRegistry\n\thasJobsPlugin: boolean\n\tevents?: FormEventSink\n\tuploads: { enabled: boolean; slug: string; collection?: CollectionConfig }\n\tspam: ResolvedSpamConfig | false\n\tshowSubmissionRawFields: boolean\n\toverrides?: {\n\t\tforms?: CollectionOverrides\n\t\tformSubmissions?: CollectionOverrides\n\t\tuploads?: CollectionOverrides\n\t}\n}\n\nexport const registerCollections = ({\n\tconfig,\n\tregistry,\n\truleRegistry,\n\tconsentRegistry,\n\tpresentationRegistry,\n\tactionRegistry,\n\thasJobsPlugin,\n\tevents,\n\tuploads,\n\tspam,\n\tshowSubmissionRawFields,\n\toverrides,\n}: RegisterCollectionsArgs): void => {\n\tregisterActionsTask(config, actionRegistry)\n\tconst hasRunner = Boolean(config.jobs?.autoRun) || hasJobsPlugin\n\n\tlet uploadsCollection: CollectionConfig | null = null\n\tif (uploads.enabled && uploads.collection) {\n\t\tconst col = uploads.collection\n\n\t\t// Spam hooks are added first (plugin-owned invariant)\n\t\tif (spam) {\n\t\t\tcol.hooks = col.hooks ?? {}\n\t\t\tcol.hooks.beforeOperation = [...(col.hooks.beforeOperation ?? []), buildUploadRateLimit(spam)]\n\t\t\tcol.hooks.beforeValidate = [...(col.hooks.beforeValidate ?? []), buildUploadOwnerStamp(spam)]\n\t\t}\n\n\t\t// Apply CollectionOverrides using explicit spreads after spam hooks are in place.\n\t\t// ...col first so the plugin defaults are the base, ...(ov ?? {}) second so the consumer\n\t\t// wins for any top-level key not explicitly re-set below (consistent with forms/formSubmissions).\n\t\t// The locked keys (hooks array ordering, fields function) are spread last.\n\t\tconst ov = overrides?.uploads\n\t\tif (ov) {\n\t\t\tconst defaultFields = (col.fields ?? []) as Field[]\n\t\t\tuploadsCollection = {\n\t\t\t\t...col,\n\t\t\t\t...(ov ?? {}),\n\t\t\t\taccess: { ...(col.access ?? {}), ...(ov.access ?? {}) },\n\t\t\t\tadmin: { ...(col.admin ?? {}), ...(ov.admin ?? {}) },\n\t\t\t\thooks: {\n\t\t\t\t\t...(ov.hooks ?? {}),\n\t\t\t\t\t// Spam hooks stay first in each array; consumer hooks appended\n\t\t\t\t\tbeforeOperation: [\n\t\t\t\t\t\t...(col.hooks?.beforeOperation ?? []),\n\t\t\t\t\t\t...(ov.hooks?.beforeOperation ?? []),\n\t\t\t\t\t],\n\t\t\t\t\tbeforeValidate: [\n\t\t\t\t\t\t...(col.hooks?.beforeValidate ?? []),\n\t\t\t\t\t\t...(ov.hooks?.beforeValidate ?? []),\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t\tfields: ov.fields ? ov.fields({ defaultFields }) : defaultFields,\n\t\t\t}\n\t\t} else {\n\t\t\tuploadsCollection = col\n\t\t}\n\t}\n\n\tconfig.collections = [\n\t\t...(config.collections ?? []),\n\t\t...(uploadsCollection ? [uploadsCollection] : []),\n\t\tbuildFormsCollection({\n\t\t\tregistry,\n\t\t\truleRegistry,\n\t\t\tconsentRegistry,\n\t\t\tpresentationRegistry,\n\t\t\tactionRegistry,\n\t\t\toverrides: overrides?.forms,\n\t\t}),\n\t\tbuildSubmissionsCollection({\n\t\t\tregistry,\n\t\t\truleRegistry,\n\t\t\tconsentRegistry,\n\t\t\tactionRegistry,\n\t\t\tevents,\n\t\t\thasRunner,\n\t\t\tuploadSlug: uploads.slug,\n\t\t\tspam,\n\t\t\tshowRawFields: showSubmissionRawFields,\n\t\t\toverrides: overrides?.formSubmissions,\n\t\t}),\n\t]\n}\n"],"mappings":";;;;;AAiCA,MAAa,uBAAuB,EACnC,QACA,UACA,cACA,iBACA,sBACA,gBACA,eACA,QACA,SACA,MACA,yBACA,gBACoC;CACpC,oBAAoB,QAAQ,cAAc;CAC1C,MAAM,YAAY,QAAQ,OAAO,MAAM,OAAO,KAAK;CAEnD,IAAI,oBAA6C;CACjD,IAAI,QAAQ,WAAW,QAAQ,YAAY;EAC1C,MAAM,MAAM,QAAQ;EAGpB,IAAI,MAAM;GACT,IAAI,QAAQ,IAAI,SAAS,CAAC;GAC1B,IAAI,MAAM,kBAAkB,CAAC,GAAI,IAAI,MAAM,mBAAmB,CAAC,GAAI,qBAAqB,IAAI,CAAC;GAC7F,IAAI,MAAM,iBAAiB,CAAC,GAAI,IAAI,MAAM,kBAAkB,CAAC,GAAI,sBAAsB,IAAI,CAAC;EAC7F;EAMA,MAAM,KAAK,WAAW;EACtB,IAAI,IAAI;GACP,MAAM,gBAAiB,IAAI,UAAU,CAAC;GACtC,oBAAoB;IACnB,GAAG;IACH,GAAI,MAAM,CAAC;IACX,QAAQ;KAAE,GAAI,IAAI,UAAU,CAAC;KAAI,GAAI,GAAG,UAAU,CAAC;IAAG;IACtD,OAAO;KAAE,GAAI,IAAI,SAAS,CAAC;KAAI,GAAI,GAAG,SAAS,CAAC;IAAG;IACnD,OAAO;KACN,GAAI,GAAG,SAAS,CAAC;KAEjB,iBAAiB,CAChB,GAAI,IAAI,OAAO,mBAAmB,CAAC,GACnC,GAAI,GAAG,OAAO,mBAAmB,CAAC,CACnC;KACA,gBAAgB,CACf,GAAI,IAAI,OAAO,kBAAkB,CAAC,GAClC,GAAI,GAAG,OAAO,kBAAkB,CAAC,CAClC;IACD;IACA,QAAQ,GAAG,SAAS,GAAG,OAAO,EAAE,cAAc,CAAC,IAAI;GACpD;EACD,OACC,oBAAoB;CAEtB;CAEA,OAAO,cAAc;EACpB,GAAI,OAAO,eAAe,CAAC;EAC3B,GAAI,oBAAoB,CAAC,iBAAiB,IAAI,CAAC;EAC/C,qBAAqB;GACpB;GACA;GACA;GACA;GACA;GACA,WAAW,WAAW;EACvB,CAAC;EACD,2BAA2B;GAC1B;GACA;GACA;GACA;GACA;GACA;GACA,YAAY,QAAQ;GACpB;GACA,eAAe;GACf,WAAW,WAAW;EACvB,CAAC;CACF;AACD"}
@@ -17,6 +17,23 @@ type FormDocument = {
17
17
  flow?: FormFlow; /** Stored presentation name; overridden by the `presentation` prop. */
18
18
  defaultPresentation?: string;
19
19
  };
20
+ /** Props passed to `renderSubmit`. */
21
+ type SubmitButtonRenderProps = {
22
+ label: string;
23
+ submitting: boolean;
24
+ };
25
+ /** Props passed to `renderNext`. */
26
+ type NextButtonRenderProps = {
27
+ label: string;
28
+ submitting: boolean;
29
+ onClick: () => void;
30
+ };
31
+ /** Props passed to `renderBack`. */
32
+ type BackButtonRenderProps = {
33
+ label: string;
34
+ submitting: boolean;
35
+ onClick: () => void;
36
+ };
20
37
  type FormProps = {
21
38
  form: FormDocument;
22
39
  fieldTypes?: AnyFormFieldDefinition[];
@@ -45,7 +62,13 @@ type FormProps = {
45
62
  }; /** A token from your captcha widget; verified server-side when a captcha provider is configured. */
46
63
  captchaToken?: string; /** Custom layout: render fields with `useField`/`useFormState` instead of the auto-rendered field loop. */
47
64
  children?: ReactNode; /** Additional CSS class names applied to the root `<form>` element (and the success node). */
48
- className?: string;
65
+ className?: string; /** Replace the default submit button entirely. Receives the resolved label and submitting state. */
66
+ renderSubmit?: (props: SubmitButtonRenderProps) => ReactNode; /** Replace the default "Next" button in multi-step forms. */
67
+ renderNext?: (props: NextButtonRenderProps) => ReactNode; /** Replace the default "Back" button in multi-step forms. */
68
+ renderBack?: (props: BackButtonRenderProps) => ReactNode; /** CSS class forwarded to the default submit `<button>`. Ignored when `renderSubmit` is provided. */
69
+ submitButtonClassName?: string; /** CSS class forwarded to the default "Next" `<button>`. Ignored when `renderNext` is provided. */
70
+ nextButtonClassName?: string; /** CSS class forwarded to the default "Back" `<button>`. Ignored when `renderBack` is provided. */
71
+ backButtonClassName?: string;
49
72
  };
50
73
  /** The headless form controller: state, progressive client validation, conditional visibility, submission, events. */
51
74
  declare const Form: ({
@@ -74,8 +97,14 @@ declare const Form: ({
74
97
  honeypot,
75
98
  captchaToken,
76
99
  children,
77
- className
100
+ className,
101
+ renderSubmit,
102
+ renderNext,
103
+ renderBack,
104
+ submitButtonClassName,
105
+ nextButtonClassName,
106
+ backButtonClassName
78
107
  }: FormProps) => import("react/jsx-runtime").JSX.Element;
79
108
  //#endregion
80
- export { Form, FormDocument, FormProps };
109
+ export { BackButtonRenderProps, Form, FormDocument, FormProps, NextButtonRenderProps, SubmitButtonRenderProps };
81
110
  //# sourceMappingURL=Form.d.ts.map
@@ -18,11 +18,11 @@ import { defaultPresentations } from "./presentation/presentations.js";
18
18
  import { resolvePresentations } from "./presentation/registry.js";
19
19
  import { applyRecall } from "./recall.js";
20
20
  import { resolveRenderers } from "./registry.js";
21
+ import { useField } from "./useField.js";
21
22
  import { defaultRenderers } from "./renderers/index.js";
22
23
  import { buildFieldTypeRegistry, buildValidationRuleRegistry, visibleFields } from "./resolveForm.js";
23
24
  import { formReducer, initialFormState } from "./state.js";
24
25
  import { submitForm } from "./submitForm.js";
25
- import { useField } from "./useField.js";
26
26
  import { validateFieldValue } from "./validateField.js";
27
27
  import { createElement, useCallback, useEffect, useId, useMemo, useReducer, useRef, useState } from "react";
28
28
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -69,7 +69,7 @@ const CalcFieldHost = ({ field, renderer, value, locale, t }) => {
69
69
  });
70
70
  };
71
71
  /** The headless form controller: state, progressive client validation, conditional visibility, submission, events. */
72
- const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSuccess, onError, events, t, locale = "en", layout, submitLabel = "Submit", nextLabel = "Next", backLabel = "Back", closeLabel = "Close", successMessage = "Thank you.", presentation, presentations, onClose, title, initialValues, honeypot, captchaToken, children, className }) => {
72
+ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSuccess, onError, events, t, locale = "en", layout, submitLabel = "Submit", nextLabel = "Next", backLabel = "Back", closeLabel = "Close", successMessage = "Thank you.", presentation, presentations, onClose, title, initialValues, honeypot, captchaToken, children, className, renderSubmit, renderNext, renderBack, submitButtonClassName, nextButtonClassName, backButtonClassName }) => {
73
73
  const honeypotName = honeypot === false ? null : honeypot?.name ?? "website";
74
74
  const honeypotRef = useRef(null);
75
75
  const registry = useMemo(() => buildFieldTypeRegistry(fieldTypes), [fieldTypes]);
@@ -264,6 +264,29 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
264
264
  }
265
265
  if (result.warnings.length > 0) warnings[result.field.name] = result.warnings;
266
266
  }
267
+ for (const field of visible.filter((f) => f.blockType === "repeater")) {
268
+ const rows = Array.isArray(effectiveValues[field.name]) ? effectiveValues[field.name] : [];
269
+ const subFields = Array.isArray(field.subFields) ? field.subFields : [];
270
+ for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
271
+ const row = rows[rowIndex] ?? {};
272
+ for (const subField of subFields) {
273
+ if (!evaluateCondition(subField.visibleWhen, row)) continue;
274
+ if (!evaluateCondition(subField.validateWhen, row)) continue;
275
+ const subResult = await validateFieldValue({
276
+ field: subField,
277
+ value: row[subField.name],
278
+ registry,
279
+ ruleRegistry,
280
+ answers: row,
281
+ locale,
282
+ t: translate
283
+ });
284
+ const compositeKey = `${field.name}[${rowIndex}].${subField.name}`;
285
+ if (subResult.errors.length > 0) errors[compositeKey] = subResult.errors;
286
+ if (subResult.warnings.length > 0) warnings[compositeKey] = subResult.warnings;
287
+ }
288
+ }
289
+ }
267
290
  rawDispatch({
268
291
  type: "SET_ALL_ISSUES",
269
292
  errors,
@@ -345,7 +368,8 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
345
368
  dispatch,
346
369
  validateField,
347
370
  locale,
348
- step
371
+ step,
372
+ rendererRegistry
349
373
  };
350
374
  const PresentationWrapper = activePresentation.Wrapper;
351
375
  const wrap = (content) => PresentationWrapper ? /* @__PURE__ */ jsx(PresentationWrapper, {
@@ -425,25 +449,43 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
425
449
  }) : null,
426
450
  flow ? /* @__PURE__ */ jsxs("div", {
427
451
  className: "fb-form__controls",
428
- children: [!step.isFirst ? /* @__PURE__ */ jsx("button", {
452
+ children: [!step.isFirst ? renderBack ? renderBack({
453
+ label: backLabel,
454
+ submitting: state.submitting,
455
+ onClick: goBack
456
+ }) : /* @__PURE__ */ jsx("button", {
429
457
  type: "button",
458
+ className: backButtonClassName,
430
459
  onClick: goBack,
431
460
  disabled: state.submitting,
432
461
  children: backLabel
433
- }) : null, step.isTerminal ? /* @__PURE__ */ jsx("button", {
462
+ }) : null, step.isTerminal ? renderSubmit ? renderSubmit({
463
+ label: submitLabel,
464
+ submitting: state.submitting
465
+ }) : /* @__PURE__ */ jsx("button", {
434
466
  type: "submit",
467
+ className: submitButtonClassName,
435
468
  disabled: state.submitting,
436
469
  children: submitLabel
470
+ }) : renderNext ? renderNext({
471
+ label: nextLabel,
472
+ submitting: state.submitting,
473
+ onClick: () => void goNext()
437
474
  }) : /* @__PURE__ */ jsx("button", {
438
475
  type: "button",
476
+ className: nextButtonClassName,
439
477
  disabled: state.submitting,
440
478
  onClick: () => {
441
479
  goNext();
442
480
  },
443
481
  children: nextLabel
444
482
  })]
483
+ }) : renderSubmit ? renderSubmit({
484
+ label: submitLabel,
485
+ submitting: state.submitting
445
486
  }) : /* @__PURE__ */ jsx("button", {
446
487
  type: "submit",
488
+ className: submitButtonClassName,
447
489
  disabled: state.submitting,
448
490
  children: submitLabel
449
491
  })