@10x-media/form-builder 0.1.0-beta.4 → 0.1.0-beta.5
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 +83 -0
- package/README.md +3 -3
- package/dist/actions/body/serializeBody.d.ts +8 -5
- package/dist/actions/body/serializeBody.js.map +1 -1
- package/dist/actions/body/textOfBody.d.ts +13 -0
- package/dist/actions/body/textOfBody.js +35 -0
- package/dist/actions/body/textOfBody.js.map +1 -0
- package/dist/actions/builtin/confirmation.js +41 -11
- package/dist/actions/builtin/confirmation.js.map +1 -1
- package/dist/actions/builtin/emailTeam.js +77 -38
- package/dist/actions/builtin/emailTeam.js.map +1 -1
- package/dist/actions/builtin/index.d.ts +8 -3
- package/dist/actions/builtin/index.js +8 -5
- package/dist/actions/builtin/index.js.map +1 -1
- package/dist/actions/defineAction.d.ts +1 -1
- package/dist/actions/fromAddresses.d.ts +21 -0
- package/dist/actions/fromAddresses.js +74 -0
- package/dist/actions/fromAddresses.js.map +1 -0
- package/dist/actions/task.js +9 -8
- package/dist/actions/task.js.map +1 -1
- package/dist/aggregation/aggregateResponses.d.ts +9 -0
- package/dist/aggregation/aggregateResponses.js +8 -14
- package/dist/aggregation/aggregateResponses.js.map +1 -1
- package/dist/aggregation/resolveResultsRequest.d.ts +19 -6
- package/dist/aggregation/resolveResultsRequest.js +58 -12
- package/dist/aggregation/resolveResultsRequest.js.map +1 -1
- package/dist/calc/computeCalcFields.js +2 -1
- package/dist/calc/computeCalcFields.js.map +1 -1
- package/dist/client/ByteSizeField.d.ts +8 -0
- package/dist/client/ByteSizeField.js +3 -1
- package/dist/client/ByteSizeField.js.map +1 -1
- package/dist/client/ClosePollButton.d.ts +15 -0
- package/dist/client/ClosePollButton.js +80 -0
- package/dist/client/ClosePollButton.js.map +1 -0
- package/dist/client/CondensedArray.d.ts +15 -0
- package/dist/client/CondensedArray.js +97 -0
- package/dist/client/CondensedArray.js.map +1 -0
- package/dist/client/EndpointOptionsSelect.d.ts +45 -0
- package/dist/client/EndpointOptionsSelect.js +103 -0
- package/dist/client/EndpointOptionsSelect.js.map +1 -0
- package/dist/client/FieldBlockLabel.d.ts +18 -0
- package/dist/client/FieldBlockLabel.js +34 -0
- package/dist/client/FieldBlockLabel.js.map +1 -0
- package/dist/client/FieldNameSelect.d.ts +20 -4
- package/dist/client/FieldNameSelect.js +25 -14
- package/dist/client/FieldNameSelect.js.map +1 -1
- package/dist/client/FlowBuilder.d.ts +2 -1
- package/dist/client/FlowBuilder.js +63 -57
- package/dist/client/FlowBuilder.js.map +1 -1
- package/dist/client/RuleDescription.d.ts +21 -0
- package/dist/client/RuleDescription.js +19 -0
- package/dist/client/RuleDescription.js.map +1 -0
- package/dist/client/admin.css +36 -0
- package/dist/client/endpointOptions.js +29 -0
- package/dist/client/endpointOptions.js.map +1 -0
- package/dist/client/flowAuthoring.js +114 -7
- package/dist/client/flowAuthoring.js.map +1 -1
- package/dist/client/synthesizeClientField.js +15 -8
- package/dist/client/synthesizeClientField.js.map +1 -1
- package/dist/collections/buttonFields.d.ts +34 -0
- package/dist/collections/buttonFields.js +35 -0
- package/dist/collections/buttonFields.js.map +1 -0
- package/dist/collections/cells/FieldCountCell.d.ts +15 -0
- package/dist/collections/cells/FieldCountCell.js +18 -0
- package/dist/collections/cells/FieldCountCell.js.map +1 -0
- package/dist/collections/cells/FlowStepsCell.d.ts +14 -0
- package/dist/collections/cells/FlowStepsCell.js +17 -0
- package/dist/collections/cells/FlowStepsCell.js.map +1 -0
- package/dist/collections/formSubmissions.js +22 -17
- package/dist/collections/formSubmissions.js.map +1 -1
- package/dist/collections/forms.js +310 -137
- package/dist/collections/forms.js.map +1 -1
- package/dist/collections/redirectFields.d.ts +29 -0
- package/dist/consent/applyConsentStatements.d.ts +14 -0
- package/dist/consent/applyConsentStatements.js +17 -0
- package/dist/consent/applyConsentStatements.js.map +1 -0
- package/dist/consent/captureConsent.d.ts +19 -8
- package/dist/consent/captureConsent.js +34 -11
- package/dist/consent/captureConsent.js.map +1 -1
- package/dist/consent/consentSourcesField.d.ts +58 -0
- package/dist/consent/consentSourcesField.js +79 -0
- package/dist/consent/consentSourcesField.js.map +1 -0
- package/dist/consent/resolveConsentEntries.d.ts +24 -0
- package/dist/consent/resolveConsentEntries.js +43 -0
- package/dist/consent/resolveConsentEntries.js.map +1 -0
- package/dist/consent/resolveConsentSourcesRequest.d.ts +37 -0
- package/dist/consent/resolveConsentSourcesRequest.js +55 -0
- package/dist/consent/resolveConsentSourcesRequest.js.map +1 -0
- package/dist/consent/resolveConsentStatements.d.ts +45 -0
- package/dist/consent/resolveConsentStatements.js +44 -0
- package/dist/consent/resolveConsentStatements.js.map +1 -0
- package/dist/consent/resolvePublishedVersionRef.d.ts +17 -11
- package/dist/consent/resolvePublishedVersionRef.js +13 -10
- package/dist/consent/resolvePublishedVersionRef.js.map +1 -1
- package/dist/consent/types.d.ts +49 -0
- package/dist/email/departments.d.ts +55 -0
- package/dist/email/departments.js +130 -0
- package/dist/email/departments.js.map +1 -0
- package/dist/email/departmentsField.d.ts +49 -0
- package/dist/email/departmentsField.js +69 -0
- package/dist/email/departmentsField.js.map +1 -0
- package/dist/exports/client.d.ts +8 -1
- package/dist/exports/client.js +8 -1
- package/dist/exports/react.d.ts +8 -3
- package/dist/exports/react.js +6 -2
- package/dist/exports/rsc.d.ts +3 -1
- package/dist/exports/rsc.js +4 -2
- package/dist/exports/types.d.ts +16 -8
- package/dist/fields/buildFieldBlocks.js +28 -13
- package/dist/fields/buildFieldBlocks.js.map +1 -1
- package/dist/fields/builtin/calculation.js +7 -0
- package/dist/fields/builtin/calculation.js.map +1 -1
- package/dist/fields/builtin/checkbox.js +8 -0
- package/dist/fields/builtin/checkbox.js.map +1 -1
- package/dist/fields/builtin/consent.js +27 -21
- package/dist/fields/builtin/consent.js.map +1 -1
- package/dist/fields/builtin/country.d.ts +13 -0
- package/dist/fields/builtin/country.js +30 -0
- package/dist/fields/builtin/country.js.map +1 -0
- package/dist/fields/builtin/date.js +2 -0
- package/dist/fields/builtin/date.js.map +1 -1
- package/dist/fields/builtin/file.js +5 -4
- package/dist/fields/builtin/file.js.map +1 -1
- package/dist/fields/builtin/index.d.ts +8 -6
- package/dist/fields/builtin/index.js +10 -9
- package/dist/fields/builtin/index.js.map +1 -1
- package/dist/fields/builtin/message.js +8 -4
- package/dist/fields/builtin/message.js.map +1 -1
- package/dist/fields/builtin/repeater.js +1 -0
- package/dist/fields/builtin/repeater.js.map +1 -1
- package/dist/fields/builtin/select.js +1 -0
- package/dist/fields/builtin/select.js.map +1 -1
- package/dist/fields/builtin/state.d.ts +13 -0
- package/dist/fields/builtin/state.js +30 -0
- package/dist/fields/builtin/state.js.map +1 -0
- package/dist/fields/data/regions.d.ts +21 -0
- package/dist/fields/data/regions.js +1238 -0
- package/dist/fields/data/regions.js.map +1 -0
- package/dist/fields/fieldKey.d.ts +15 -0
- package/dist/fields/fieldKey.js +16 -0
- package/dist/fields/fieldKey.js.map +1 -0
- package/dist/fields/fieldNamesOfType.js +13 -8
- package/dist/fields/fieldNamesOfType.js.map +1 -1
- package/dist/fields/instanceOptions.js +20 -0
- package/dist/fields/instanceOptions.js.map +1 -0
- package/dist/fields/registry.d.ts +1 -0
- package/dist/fields/registry.js +21 -2
- package/dist/fields/registry.js.map +1 -1
- package/dist/fields/sharedConfig.js +46 -9
- package/dist/fields/sharedConfig.js.map +1 -1
- package/dist/fields/types.d.ts +54 -3
- package/dist/flow/engine.d.ts +6 -4
- package/dist/flow/engine.js +11 -6
- package/dist/flow/engine.js.map +1 -1
- package/dist/flow/normalizeFlow.js +16 -7
- package/dist/flow/normalizeFlow.js.map +1 -1
- package/dist/flow/types.d.ts +12 -3
- package/dist/flow/types.js +11 -0
- package/dist/flow/types.js.map +1 -0
- package/dist/form/pollState.d.ts +4 -4
- package/dist/form/pollState.js.map +1 -1
- package/dist/form/toFormDocument.d.ts +37 -12
- package/dist/form/toFormDocument.js +43 -7
- package/dist/form/toFormDocument.js.map +1 -1
- package/dist/form/types.d.ts +31 -14
- package/dist/index.d.ts +143 -28
- package/dist/index.js +43 -14
- package/dist/index.js.map +1 -1
- package/dist/plugin/customState.js +26 -0
- package/dist/plugin/customState.js.map +1 -0
- package/dist/plugin/registerCollections.js +23 -13
- package/dist/plugin/registerCollections.js.map +1 -1
- package/dist/plugin/uploadsCollection.d.ts +2 -0
- package/dist/poll/applyPollOptions.js +3 -2
- package/dist/poll/applyPollOptions.js.map +1 -1
- package/dist/poll/buildPollOptionSourceFields.js +2 -5
- package/dist/poll/buildPollOptionSourceFields.js.map +1 -1
- package/dist/poll/closeJob.d.ts +52 -0
- package/dist/poll/closeJob.js +119 -0
- package/dist/poll/closeJob.js.map +1 -0
- package/dist/poll/definePollOptionSource.d.ts +10 -1
- package/dist/poll/definePollOptionSource.js.map +1 -1
- package/dist/poll/definePollType.d.ts +34 -0
- package/dist/poll/definePollType.js +6 -0
- package/dist/poll/definePollType.js.map +1 -0
- package/dist/poll/effectivePollOptions.d.ts +38 -0
- package/dist/poll/effectivePollOptions.js +64 -0
- package/dist/poll/effectivePollOptions.js.map +1 -0
- package/dist/poll/mostVoted.d.ts +21 -0
- package/dist/poll/mostVoted.js +32 -0
- package/dist/poll/mostVoted.js.map +1 -0
- package/dist/poll/outcomeBeforeChange.js +91 -0
- package/dist/poll/outcomeBeforeChange.js.map +1 -0
- package/dist/poll/outcomeFields.d.ts +38 -0
- package/dist/poll/outcomeFields.js +52 -0
- package/dist/poll/outcomeFields.js.map +1 -0
- package/dist/poll/pollTypeRegistry.d.ts +34 -0
- package/dist/poll/pollTypeRegistry.js +71 -0
- package/dist/poll/pollTypeRegistry.js.map +1 -0
- package/dist/poll/resolvePollCloseRequest.js +105 -0
- package/dist/poll/resolvePollCloseRequest.js.map +1 -0
- package/dist/poll/resolvePollOptions.d.ts +2 -1
- package/dist/poll/resolvePollOptions.js +5 -17
- package/dist/poll/resolvePollOptions.js.map +1 -1
- package/dist/poll/resolvePollOptionsRequest.js +51 -0
- package/dist/poll/resolvePollOptionsRequest.js.map +1 -0
- package/dist/poll/resolvePollOutcome.d.ts +22 -9
- package/dist/poll/resolvePollOutcome.js +54 -14
- package/dist/poll/resolvePollOutcome.js.map +1 -1
- package/dist/poll/resultsField.js +21 -0
- package/dist/poll/resultsField.js.map +1 -0
- package/dist/prefill/valuesFromSearchParams.js +2 -1
- package/dist/prefill/valuesFromSearchParams.js.map +1 -1
- package/dist/react/Form.d.ts +8 -8
- package/dist/react/Form.js +74 -62
- package/dist/react/Form.js.map +1 -1
- package/dist/react/FormContext.d.ts +6 -4
- package/dist/react/FormContext.js.map +1 -1
- package/dist/react/FormControls.d.ts +3 -3
- package/dist/react/FormControls.js +3 -3
- package/dist/react/FormControls.js.map +1 -1
- package/dist/react/FormResults.d.ts +5 -5
- package/dist/react/FormResults.js +6 -6
- package/dist/react/FormResults.js.map +1 -1
- package/dist/react/Poll.d.ts +2 -2
- package/dist/react/Poll.js +5 -5
- package/dist/react/Poll.js.map +1 -1
- package/dist/react/contract.d.ts +2 -3
- package/dist/react/contract.js.map +1 -1
- package/dist/react/primitives/Checkbox.d.ts +4 -2
- package/dist/react/primitives/Checkbox.js +2 -1
- package/dist/react/primitives/Checkbox.js.map +1 -1
- package/dist/react/primitives/FieldShell.d.ts +2 -4
- package/dist/react/primitives/FieldShell.js +12 -19
- package/dist/react/primitives/FieldShell.js.map +1 -1
- package/dist/react/renderers/calculation.js +1 -2
- package/dist/react/renderers/calculation.js.map +1 -1
- package/dist/react/renderers/checkbox.js +1 -2
- package/dist/react/renderers/checkbox.js.map +1 -1
- package/dist/react/renderers/consent.js +78 -34
- package/dist/react/renderers/consent.js.map +1 -1
- package/dist/react/renderers/date.js +1 -3
- package/dist/react/renderers/date.js.map +1 -1
- package/dist/react/renderers/email.js +1 -2
- package/dist/react/renderers/email.js.map +1 -1
- package/dist/react/renderers/file.js +1 -2
- package/dist/react/renderers/file.js.map +1 -1
- package/dist/react/renderers/index.js +3 -0
- package/dist/react/renderers/index.js.map +1 -1
- package/dist/react/renderers/number.js +1 -2
- package/dist/react/renderers/number.js.map +1 -1
- package/dist/react/renderers/region.js +45 -0
- package/dist/react/renderers/region.js.map +1 -0
- package/dist/react/renderers/repeater.js +7 -10
- package/dist/react/renderers/repeater.js.map +1 -1
- package/dist/react/renderers/select.js +1 -2
- package/dist/react/renderers/select.js.map +1 -1
- package/dist/react/renderers/text.js +1 -2
- package/dist/react/renderers/text.js.map +1 -1
- package/dist/react/renderers/textarea.js +1 -2
- package/dist/react/renderers/textarea.js.map +1 -1
- package/dist/react/state.d.ts +0 -3
- package/dist/react/state.js +7 -11
- package/dist/react/state.js.map +1 -1
- package/dist/react/useField.d.ts +0 -1
- package/dist/react/useField.js +0 -1
- package/dist/react/useField.js.map +1 -1
- package/dist/react/useFormState.d.ts +1 -1
- package/dist/react/useFormState.js +1 -1
- package/dist/react/useFormState.js.map +1 -1
- package/dist/react/validateField.js +3 -6
- package/dist/react/validateField.js.map +1 -1
- package/dist/recall/interpolate.d.ts +4 -3
- package/dist/recall/interpolate.js +5 -4
- package/dist/recall/interpolate.js.map +1 -1
- package/dist/recall/resolver.d.ts +1 -1
- package/dist/recall/resolver.js +14 -2
- package/dist/recall/resolver.js.map +1 -1
- package/dist/spam/captcha.d.ts +1 -1
- package/dist/spam/captcha.js +1 -1
- package/dist/spam/captcha.js.map +1 -1
- package/dist/submissions/SubmissionAnswers.js +6 -2
- package/dist/submissions/SubmissionAnswers.js.map +1 -1
- package/dist/submissions/SubmissionAnswersClient.js +91 -62
- package/dist/submissions/SubmissionAnswersClient.js.map +1 -1
- package/dist/submissions/createSubmission.d.ts +36 -0
- package/dist/submissions/createSubmission.js +26 -0
- package/dist/submissions/createSubmission.js.map +1 -0
- package/dist/submissions/runSubmission.d.ts +10 -0
- package/dist/submissions/runSubmission.js +29 -18
- package/dist/submissions/runSubmission.js.map +1 -1
- package/dist/submissions/types.d.ts +11 -4
- package/dist/submissions/validateSubmission.js +38 -23
- package/dist/submissions/validateSubmission.js.map +1 -1
- package/dist/submissions/votedCookie.js +3 -3
- package/dist/submissions/votedCookie.js.map +1 -1
- package/dist/translations/de.js +102 -26
- package/dist/translations/de.js.map +1 -1
- package/dist/translations/en.js +103 -27
- package/dist/translations/en.js.map +1 -1
- package/dist/translations/keys.d.ts +97 -21
- package/dist/translations/keys.js +98 -22
- package/dist/translations/keys.js.map +1 -1
- package/dist/validation/buildRuleBlocks.js +18 -17
- package/dist/validation/buildRuleBlocks.js.map +1 -1
- package/dist/validation/builtin/email.js +1 -0
- package/dist/validation/builtin/email.js.map +1 -1
- package/dist/validation/builtin/matchesField.js +3 -7
- package/dist/validation/builtin/matchesField.js.map +1 -1
- package/dist/validation/builtin/max.js +1 -0
- package/dist/validation/builtin/max.js.map +1 -1
- package/dist/validation/builtin/maxDate.js +1 -0
- package/dist/validation/builtin/maxDate.js.map +1 -1
- package/dist/validation/builtin/maxLength.js +1 -0
- package/dist/validation/builtin/maxLength.js.map +1 -1
- package/dist/validation/builtin/min.js +1 -0
- package/dist/validation/builtin/min.js.map +1 -1
- package/dist/validation/builtin/minDate.js +1 -0
- package/dist/validation/builtin/minDate.js.map +1 -1
- package/dist/validation/builtin/minLength.js +1 -0
- package/dist/validation/builtin/minLength.js.map +1 -1
- package/dist/validation/builtin/notAlreadySubmitted.js +1 -0
- package/dist/validation/builtin/notAlreadySubmitted.js.map +1 -1
- package/dist/validation/builtin/oneOf.js +1 -0
- package/dist/validation/builtin/oneOf.js.map +1 -1
- package/dist/validation/builtin/pattern.js +11 -2
- package/dist/validation/builtin/pattern.js.map +1 -1
- package/dist/validation/builtin/url.js +1 -0
- package/dist/validation/builtin/url.js.map +1 -1
- package/dist/validation/builtin/validateRegexParams.js +44 -0
- package/dist/validation/builtin/validateRegexParams.js.map +1 -0
- package/dist/validation/fieldTargetParam.d.ts +22 -0
- package/dist/validation/fieldTargetParam.js +38 -0
- package/dist/validation/fieldTargetParam.js.map +1 -0
- package/dist/validation/runValidation.js +8 -29
- package/dist/validation/runValidation.js.map +1 -1
- package/dist/validation/types.d.ts +5 -11
- package/package.json +6 -6
- package/dist/consent/buildConsentSourceConfig.js +0 -49
- package/dist/consent/buildConsentSourceConfig.js.map +0 -1
- package/dist/consent/builtin/index.d.ts +0 -17
- package/dist/consent/builtin/index.js +0 -21
- package/dist/consent/builtin/index.js.map +0 -1
- package/dist/consent/builtin/pageReference.js +0 -75
- package/dist/consent/builtin/pageReference.js.map +0 -1
- package/dist/consent/builtin/static.js +0 -48
- package/dist/consent/builtin/static.js.map +0 -1
- package/dist/consent/defineConsentSource.d.ts +0 -35
- package/dist/consent/defineConsentSource.js +0 -6
- package/dist/consent/defineConsentSource.js.map +0 -1
- package/dist/consent/registry.d.ts +0 -16
- package/dist/consent/registry.js +0 -16
- package/dist/consent/registry.js.map +0 -1
- package/dist/consent/resolveConsentLinks.d.ts +0 -19
- package/dist/consent/resolveConsentLinks.js +0 -26
- package/dist/consent/resolveConsentLinks.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/actions/builtin/index.ts"],"sourcesContent":["import type { RichTextField } from 'payload'\nimport type { AnyActionDefinition } from '../defineAction'\nimport { buildConfirmation } from './confirmation'\nimport { buildEmailTeam } from './emailTeam'\nimport { signedWebhook } from './signedWebhook'\n\n/**\n * Built-in action definitions with content-bearing config fields (email subjects and rich text\n * bodies) carrying `localized: true` when `localize` is true. `editor`, when given, overrides\n * the Lexical/richText editor on both action bodies (from the plugin's `
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/actions/builtin/index.ts"],"sourcesContent":["import type { RichTextField } from 'payload'\nimport type { DepartmentEmailsResolver } from '../../email/departments'\nimport type { AnyActionDefinition } from '../defineAction'\nimport type { FromAddressesResolver } from '../fromAddresses'\nimport { buildConfirmation } from './confirmation'\nimport { buildEmailTeam } from './emailTeam'\nimport { signedWebhook } from './signedWebhook'\n\n/**\n * Built-in action definitions with content-bearing config fields (email subjects and rich text\n * bodies) carrying `localized: true` when `localize` is true. `editor`, when given, overrides\n * the Lexical/richText editor on both action bodies (the plugin passes\n * `richText.bodyEditor ?? richText.editor`). `fromAddresses`, when given (the plugin's\n * `email.fromAddresses` option), adds a `from` select to both email actions. `departments`, when\n * given (the plugin's `email.departments` option), turns the `emailTeam` `to` into a select over the\n * host's resolved departments. `signedWebhook` has no content fields and is shared static.\n */\n// biome-ignore lint/complexity/useMaxParams: positional args thread the plugin editor and email resolvers (localize, editor, fromAddresses, departments)\nexport const buildDefaultActionDefinitions = (\n\tlocalize: boolean,\n\teditor?: RichTextField['editor'],\n\tfromAddresses?: FromAddressesResolver,\n\tdepartments?: DepartmentEmailsResolver\n): Record<string, AnyActionDefinition> => ({\n\temailTeam: buildEmailTeam(localize, editor, fromAddresses, departments) as AnyActionDefinition,\n\tconfirmation: buildConfirmation(localize, editor, fromAddresses) as AnyActionDefinition,\n\tsignedWebhook: signedWebhook as AnyActionDefinition,\n})\n\n/**\n * Prebuilt with content localization on; with `localizeContent: false`, derive overrides from\n * `buildDefaultActionDefinitions(false)` instead so a spread does not reintroduce `localized`.\n */\nexport const defaultActionDefinitions: Record<string, AnyActionDefinition> =\n\tbuildDefaultActionDefinitions(true)\n"],"mappings":";;;;;;;;;;;;;AAkBA,MAAa,iCACZ,UACA,QACA,eACA,iBAC0C;CAC1C,WAAW,eAAe,UAAU,QAAQ,eAAe,WAAW;CACtE,cAAc,kBAAkB,UAAU,QAAQ,aAAa;CAChD;AAChB;;;;;AAMA,MAAa,2BACZ,8BAA8B,IAAI"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Translate } from "../fields/types.js";
|
|
2
1
|
import { SubmissionDescriptor, SubmissionValue } from "../submissions/types.js";
|
|
2
|
+
import { Translate } from "../fields/types.js";
|
|
3
3
|
import { Field, Payload, PayloadRequest } from "payload";
|
|
4
4
|
|
|
5
5
|
//#region src/actions/defineAction.d.ts
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { PayloadRequest } from "payload";
|
|
2
|
+
|
|
3
|
+
//#region src/actions/fromAddresses.d.ts
|
|
4
|
+
/** One selectable "from" address for the built-in email actions. */
|
|
5
|
+
type FromAddressOption = {
|
|
6
|
+
label: string;
|
|
7
|
+
value: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Host seam resolving the selectable `from` addresses for `emailTeam`/`confirmation`
|
|
11
|
+
* (plugin option `email.fromAddresses`). Multi-tenant hosts derive tenant scoping from `req`
|
|
12
|
+
* (host header, cookie, or auth context) and return only that tenant's allowed senders. `value`
|
|
13
|
+
* is the literal string handed to `payload.sendEmail`'s `from` (e.g. `'Name <addr@x.com>'` or a
|
|
14
|
+
* plain address). Absent keeps the email adapter's default sender and adds no `from` field at all.
|
|
15
|
+
*/
|
|
16
|
+
type FromAddressesResolver = (args: {
|
|
17
|
+
req: PayloadRequest;
|
|
18
|
+
}) => Promise<FromAddressOption[]> | FromAddressOption[];
|
|
19
|
+
//#endregion
|
|
20
|
+
export { FromAddressOption, FromAddressesResolver };
|
|
21
|
+
//# sourceMappingURL=fromAddresses.d.ts.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { keys } from "../translations/keys.js";
|
|
2
|
+
import { asTranslate, labelFor } from "../translations/server.js";
|
|
3
|
+
//#region src/actions/fromAddresses.ts
|
|
4
|
+
const FROM_FIELD_REF = "@10x-media/form-builder/client#EndpointOptionsSelect";
|
|
5
|
+
/**
|
|
6
|
+
* Validate for the `from` field, closed over the host resolver (mirrors the confirmation action's
|
|
7
|
+
* `toField` and poll's `resultsField`): unset is fine, otherwise the value must be one of the
|
|
8
|
+
* resolver's options for this request. A throwing resolver fails closed with a translated message
|
|
9
|
+
* rather than surfacing a raw error on save.
|
|
10
|
+
*
|
|
11
|
+
* Failing closed has an operational cost worth knowing: Payload runs this on every save, not only
|
|
12
|
+
* when `from` changed, so for as long as the resolver is down no form carrying an email action with
|
|
13
|
+
* a `from` set can be saved at all, including edits that never touch the address. That is the
|
|
14
|
+
* deliberate trade: failing open would persist a sender the host can no longer vouch for, and
|
|
15
|
+
* unlike `toField` and `resultsField` this seam depends on host infrastructure that can be down.
|
|
16
|
+
* A resolver reaching a flaky upstream should cache or fall back internally rather than throw.
|
|
17
|
+
*/
|
|
18
|
+
const validateFromField = (resolver) => async (value, { req }) => {
|
|
19
|
+
if (typeof value !== "string" || value.length === 0) return true;
|
|
20
|
+
let options;
|
|
21
|
+
try {
|
|
22
|
+
options = await resolver({ req });
|
|
23
|
+
} catch {
|
|
24
|
+
return asTranslate(req.t)(keys.validationFromUnavailable);
|
|
25
|
+
}
|
|
26
|
+
return options.some((option) => option.value === value) ? true : asTranslate(req.t)(keys.validationFromUnknown);
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* The `from` select shared by `emailTeam` and `confirmation`: an `EndpointOptionsSelect` backed by
|
|
30
|
+
* the forms collection's `/:id/from-addresses` endpoint (registered only when `email.fromAddresses`
|
|
31
|
+
* is set). That route's document id goes unused server-side: the option set is request-scoped, not
|
|
32
|
+
* per-form, so this reuses the existing doc-scoped component as-is instead of adding an id-less mode.
|
|
33
|
+
*/
|
|
34
|
+
const buildFromField = (resolver) => ({
|
|
35
|
+
name: "from",
|
|
36
|
+
type: "text",
|
|
37
|
+
label: labelFor(keys.actionConfigFrom),
|
|
38
|
+
validate: validateFromField(resolver),
|
|
39
|
+
admin: { components: { Field: {
|
|
40
|
+
path: FROM_FIELD_REF,
|
|
41
|
+
clientProps: {
|
|
42
|
+
endpoint: "from-addresses",
|
|
43
|
+
descriptionKey: keys.actionConfigFromDescription
|
|
44
|
+
}
|
|
45
|
+
} } }
|
|
46
|
+
});
|
|
47
|
+
/**
|
|
48
|
+
* Authorize and resolve the `GET /:id/from-addresses` request backing the `from` selects:
|
|
49
|
+
* authenticated callers get the host resolver's current options for this request; anonymous
|
|
50
|
+
* callers are always refused. The route id is unused (see `buildFromField`). Statuses mirror
|
|
51
|
+
* the poll-options endpoint: 403 unauthenticated, 503 when the resolver throws (fail closed).
|
|
52
|
+
*/
|
|
53
|
+
const resolveFromAddressesRequest = async (args) => {
|
|
54
|
+
const { isAuthed, req, resolver } = args;
|
|
55
|
+
if (!isAuthed) return {
|
|
56
|
+
status: 403,
|
|
57
|
+
body: { errors: [{ message: "Forbidden" }] }
|
|
58
|
+
};
|
|
59
|
+
try {
|
|
60
|
+
return {
|
|
61
|
+
status: 200,
|
|
62
|
+
body: { options: await resolver({ req }) }
|
|
63
|
+
};
|
|
64
|
+
} catch {
|
|
65
|
+
return {
|
|
66
|
+
status: 503,
|
|
67
|
+
body: { errors: [{ message: "From addresses unavailable" }] }
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
//#endregion
|
|
72
|
+
export { buildFromField, resolveFromAddressesRequest };
|
|
73
|
+
|
|
74
|
+
//# sourceMappingURL=fromAddresses.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fromAddresses.js","names":[],"sources":["../../src/actions/fromAddresses.ts"],"sourcesContent":["import type { PayloadRequest, TextField } from 'payload'\nimport { keys } from '../translations/keys'\nimport { asTranslate, labelFor } from '../translations/server'\n\n/** One selectable \"from\" address for the built-in email actions. */\nexport type FromAddressOption = { label: string; value: string }\n\n/**\n * Host seam resolving the selectable `from` addresses for `emailTeam`/`confirmation`\n * (plugin option `email.fromAddresses`). Multi-tenant hosts derive tenant scoping from `req`\n * (host header, cookie, or auth context) and return only that tenant's allowed senders. `value`\n * is the literal string handed to `payload.sendEmail`'s `from` (e.g. `'Name <addr@x.com>'` or a\n * plain address). Absent keeps the email adapter's default sender and adds no `from` field at all.\n */\nexport type FromAddressesResolver = (args: {\n\treq: PayloadRequest\n}) => Promise<FromAddressOption[]> | FromAddressOption[]\n\nconst FROM_FIELD_REF = '@10x-media/form-builder/client#EndpointOptionsSelect'\n\n/**\n * Validate for the `from` field, closed over the host resolver (mirrors the confirmation action's\n * `toField` and poll's `resultsField`): unset is fine, otherwise the value must be one of the\n * resolver's options for this request. A throwing resolver fails closed with a translated message\n * rather than surfacing a raw error on save.\n *\n * Failing closed has an operational cost worth knowing: Payload runs this on every save, not only\n * when `from` changed, so for as long as the resolver is down no form carrying an email action with\n * a `from` set can be saved at all, including edits that never touch the address. That is the\n * deliberate trade: failing open would persist a sender the host can no longer vouch for, and\n * unlike `toField` and `resultsField` this seam depends on host infrastructure that can be down.\n * A resolver reaching a flaky upstream should cache or fall back internally rather than throw.\n */\nexport const validateFromField =\n\t(resolver: FromAddressesResolver) =>\n\tasync (value: unknown, { req }: { req: PayloadRequest }): Promise<string | true> => {\n\t\tif (typeof value !== 'string' || value.length === 0) {\n\t\t\treturn true\n\t\t}\n\t\tlet options: FromAddressOption[]\n\t\ttry {\n\t\t\toptions = await resolver({ req })\n\t\t} catch {\n\t\t\treturn asTranslate(req.t)(keys.validationFromUnavailable)\n\t\t}\n\t\treturn options.some((option) => option.value === value)\n\t\t\t? true\n\t\t\t: asTranslate(req.t)(keys.validationFromUnknown)\n\t}\n\n/**\n * The `from` select shared by `emailTeam` and `confirmation`: an `EndpointOptionsSelect` backed by\n * the forms collection's `/:id/from-addresses` endpoint (registered only when `email.fromAddresses`\n * is set). That route's document id goes unused server-side: the option set is request-scoped, not\n * per-form, so this reuses the existing doc-scoped component as-is instead of adding an id-less mode.\n */\nexport const buildFromField = (resolver: FromAddressesResolver): TextField => ({\n\tname: 'from',\n\ttype: 'text',\n\tlabel: labelFor(keys.actionConfigFrom),\n\tvalidate: validateFromField(resolver),\n\tadmin: {\n\t\tcomponents: {\n\t\t\tField: {\n\t\t\t\tpath: FROM_FIELD_REF,\n\t\t\t\tclientProps: {\n\t\t\t\t\tendpoint: 'from-addresses',\n\t\t\t\t\tdescriptionKey: keys.actionConfigFromDescription,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n})\n\nexport type ResolveFromAddressesRequestArgs = {\n\t/** Whether the caller is authenticated (an admin/user). */\n\tisAuthed: boolean\n\treq: PayloadRequest\n\tresolver: FromAddressesResolver\n}\n\nexport type ResolveFromAddressesRequestResult = {\n\tstatus: number\n\tbody: { options: FromAddressOption[] } | { errors: { message: string }[] }\n}\n\n/**\n * Authorize and resolve the `GET /:id/from-addresses` request backing the `from` selects:\n * authenticated callers get the host resolver's current options for this request; anonymous\n * callers are always refused. The route id is unused (see `buildFromField`). Statuses mirror\n * the poll-options endpoint: 403 unauthenticated, 503 when the resolver throws (fail closed).\n */\nexport const resolveFromAddressesRequest = async (\n\targs: ResolveFromAddressesRequestArgs\n): Promise<ResolveFromAddressesRequestResult> => {\n\tconst { isAuthed, req, resolver } = args\n\tif (!isAuthed) {\n\t\treturn { status: 403, body: { errors: [{ message: 'Forbidden' }] } }\n\t}\n\ttry {\n\t\tconst options = await resolver({ req })\n\t\treturn { status: 200, body: { options } }\n\t} catch {\n\t\treturn { status: 503, body: { errors: [{ message: 'From addresses unavailable' }] } }\n\t}\n}\n"],"mappings":";;;AAkBA,MAAM,iBAAiB;;;;;;;;;;;;;;AAevB,MAAa,qBACX,aACD,OAAO,OAAgB,EAAE,UAA2D;CACnF,IAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GACjD,OAAO;CAER,IAAI;CACJ,IAAI;EACH,UAAU,MAAM,SAAS,EAAE,IAAI,CAAC;CACjC,QAAQ;EACP,OAAO,YAAY,IAAI,CAAC,EAAE,KAAK,yBAAyB;CACzD;CACA,OAAO,QAAQ,MAAM,WAAW,OAAO,UAAU,KAAK,IACnD,OACA,YAAY,IAAI,CAAC,EAAE,KAAK,qBAAqB;AACjD;;;;;;;AAQD,MAAa,kBAAkB,cAAgD;CAC9E,MAAM;CACN,MAAM;CACN,OAAO,SAAS,KAAK,gBAAgB;CACrC,UAAU,kBAAkB,QAAQ;CACpC,OAAO,EACN,YAAY,EACX,OAAO;EACN,MAAM;EACN,aAAa;GACZ,UAAU;GACV,gBAAgB,KAAK;EACtB;CACD,EACD,EACD;AACD;;;;;;;AAoBA,MAAa,8BAA8B,OAC1C,SACgD;CAChD,MAAM,EAAE,UAAU,KAAK,aAAa;CACpC,IAAI,CAAC,UACJ,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,EAAE;CAAE;CAEpE,IAAI;EAEH,OAAO;GAAE,QAAQ;GAAK,MAAM,EAAE,SAAA,MADR,SAAS,EAAE,IAAI,CAAC,EACA;EAAE;CACzC,QAAQ;EACP,OAAO;GAAE,QAAQ;GAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,6BAA6B,CAAC,EAAE;EAAE;CACrF;AACD"}
|
package/dist/actions/task.js
CHANGED
|
@@ -14,14 +14,6 @@ const asDescriptors = (value) => Array.isArray(value) ? value : [];
|
|
|
14
14
|
*/
|
|
15
15
|
const runActionsForSubmission = async (args) => {
|
|
16
16
|
const { input, registry, payload, req, richText } = args;
|
|
17
|
-
const form = await payload.findByID({
|
|
18
|
-
collection: FORMS_SLUG,
|
|
19
|
-
id: input.formId,
|
|
20
|
-
depth: 0,
|
|
21
|
-
overrideAccess: true,
|
|
22
|
-
req
|
|
23
|
-
}).catch(() => null);
|
|
24
|
-
if (!form) return;
|
|
25
17
|
const submission = await payload.findByID({
|
|
26
18
|
collection: FORM_SUBMISSIONS_SLUG,
|
|
27
19
|
id: input.submissionId,
|
|
@@ -31,6 +23,15 @@ const runActionsForSubmission = async (args) => {
|
|
|
31
23
|
}).catch(() => null);
|
|
32
24
|
if (!submission) return;
|
|
33
25
|
const locale = typeof submission.locale === "string" ? submission.locale : req?.locale ?? "en";
|
|
26
|
+
const form = await payload.findByID({
|
|
27
|
+
collection: FORMS_SLUG,
|
|
28
|
+
id: input.formId,
|
|
29
|
+
depth: 0,
|
|
30
|
+
overrideAccess: true,
|
|
31
|
+
locale,
|
|
32
|
+
req
|
|
33
|
+
}).catch(() => null);
|
|
34
|
+
if (!form) return;
|
|
34
35
|
const t = asFieldTranslate(req?.i18n?.t ?? ((key) => key));
|
|
35
36
|
await runActions({
|
|
36
37
|
actions: asActions(form.actions),
|
package/dist/actions/task.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task.js","names":[],"sources":["../../src/actions/task.ts"],"sourcesContent":["import type { Config, Payload, PayloadRequest, TaskConfig } from 'payload'\nimport { FORM_SUBMISSIONS_SLUG } from '../collections/formSubmissions'\nimport { FORMS_SLUG } from '../collections/forms'\nimport type { Translate } from '../fields/types'\nimport type { SubmissionDescriptor, SubmissionValue } from '../submissions/types'\nimport { asFieldTranslate } from '../translations/server'\nimport type { RichTextBodyOption } from './body/serializeBody'\nimport type { ActionRegistry } from './registry'\nimport type { ActionInstance } from './runActions'\nimport { runActions } from './runActions'\n\nexport const ACTIONS_TASK_SLUG = 'form-builder-actions'\n\n/** Input the dispatch path enqueues; the handler re-loads everything else from the DB. */\nexport type ActionsTaskInput = { formId: number | string; submissionId: number | string }\n\nconst asActions = (value: unknown): ActionInstance[] =>\n\tArray.isArray(value) ? (value as ActionInstance[]) : []\n\nconst asValues = (value: unknown): SubmissionValue[] =>\n\tArray.isArray(value) ? (value as SubmissionValue[]) : []\n\nconst asDescriptors = (value: unknown): SubmissionDescriptor[] =>\n\tArray.isArray(value) ? (value as SubmissionDescriptor[]) : []\n\n/**\n * Load the form and submission by id and run the form's actions through the shared, failure-isolating\n * `runActions`. Tolerates a missing form or submission (the row may have been deleted between enqueue and\n * run) by returning early. Used by both the queued task handler and the inline fallback.\n */\nexport const runActionsForSubmission = async (args: {\n\tinput: ActionsTaskInput\n\tregistry: ActionRegistry\n\tpayload: Payload\n\treq?: PayloadRequest\n\trichText?: RichTextBodyOption\n}): Promise<void> => {\n\tconst { input, registry, payload, req, richText } = args\n\tconst
|
|
1
|
+
{"version":3,"file":"task.js","names":[],"sources":["../../src/actions/task.ts"],"sourcesContent":["import type { Config, Payload, PayloadRequest, TaskConfig, TypedLocale } from 'payload'\nimport { FORM_SUBMISSIONS_SLUG } from '../collections/formSubmissions'\nimport { FORMS_SLUG } from '../collections/forms'\nimport type { Translate } from '../fields/types'\nimport type { SubmissionDescriptor, SubmissionValue } from '../submissions/types'\nimport { asFieldTranslate } from '../translations/server'\nimport type { RichTextBodyOption } from './body/serializeBody'\nimport type { ActionRegistry } from './registry'\nimport type { ActionInstance } from './runActions'\nimport { runActions } from './runActions'\n\nexport const ACTIONS_TASK_SLUG = 'form-builder-actions'\n\n/** Input the dispatch path enqueues; the handler re-loads everything else from the DB. */\nexport type ActionsTaskInput = { formId: number | string; submissionId: number | string }\n\nconst asActions = (value: unknown): ActionInstance[] =>\n\tArray.isArray(value) ? (value as ActionInstance[]) : []\n\nconst asValues = (value: unknown): SubmissionValue[] =>\n\tArray.isArray(value) ? (value as SubmissionValue[]) : []\n\nconst asDescriptors = (value: unknown): SubmissionDescriptor[] =>\n\tArray.isArray(value) ? (value as SubmissionDescriptor[]) : []\n\n/**\n * Load the form and submission by id and run the form's actions through the shared, failure-isolating\n * `runActions`. Tolerates a missing form or submission (the row may have been deleted between enqueue and\n * run) by returning early. Used by both the queued task handler and the inline fallback.\n */\nexport const runActionsForSubmission = async (args: {\n\tinput: ActionsTaskInput\n\tregistry: ActionRegistry\n\tpayload: Payload\n\treq?: PayloadRequest\n\trichText?: RichTextBodyOption\n}): Promise<void> => {\n\tconst { input, registry, payload, req, richText } = args\n\tconst submission = await payload\n\t\t.findByID({\n\t\t\tcollection: FORM_SUBMISSIONS_SLUG,\n\t\t\tid: input.submissionId,\n\t\t\tdepth: 0,\n\t\t\toverrideAccess: true,\n\t\t\treq,\n\t\t})\n\t\t.catch(() => null)\n\tif (!submission) {\n\t\treturn\n\t}\n\n\t// The submission's own stored locale (set from req.locale at submit) is authoritative, so the form\n\t// is loaded at it. A localized action config, notably the emailTeam `to`, then resolves to the\n\t// submission's locale even on the queued path, where the job runner's req may carry a different\n\t// (or no) locale than the visitor who submitted.\n\tconst locale = typeof submission.locale === 'string' ? submission.locale : (req?.locale ?? 'en')\n\n\tconst form = await payload\n\t\t.findByID({\n\t\t\tcollection: FORMS_SLUG,\n\t\t\tid: input.formId,\n\t\t\tdepth: 0,\n\t\t\toverrideAccess: true,\n\t\t\t// Cast: the stored locale is a plain string; a host's concrete locale union is unknowable from\n\t\t\t// the plugin, and an unrecognized code just falls back on read, so this narrows (zero runtime\n\t\t\t// delta) to satisfy a host whose `findByID` locale is a real union.\n\t\t\tlocale: locale as TypedLocale,\n\t\t\treq,\n\t\t})\n\t\t.catch(() => null)\n\tif (!form) {\n\t\treturn\n\t}\n\n\tconst t: Translate = asFieldTranslate(req?.i18n?.t ?? ((key: string) => key))\n\n\tawait runActions({\n\t\tactions: asActions(form.actions),\n\t\tregistry,\n\t\trichText,\n\t\tform: { id: form.id, title: typeof form.title === 'string' ? form.title : undefined },\n\t\tsubmissionId: submission.id,\n\t\tvalues: asValues(submission.values),\n\t\tdescriptors: asDescriptors(submission.descriptors),\n\t\tpayload,\n\t\treq,\n\t\tlocale,\n\t\tt,\n\t})\n}\n\n/** Native Payload jobs task that runs a submission's post-submit actions out of band. */\nexport const buildActionsTask = (\n\tregistry: ActionRegistry,\n\trichText?: RichTextBodyOption\n): TaskConfig =>\n\t({\n\t\tslug: ACTIONS_TASK_SLUG,\n\t\tinputSchema: [\n\t\t\t{ name: 'formId', type: 'text', required: true },\n\t\t\t{ name: 'submissionId', type: 'text', required: true },\n\t\t],\n\t\thandler: async ({ input, req }) => {\n\t\t\tawait runActionsForSubmission({\n\t\t\t\tinput: input as ActionsTaskInput,\n\t\t\t\tregistry,\n\t\t\t\tpayload: req.payload,\n\t\t\t\treq,\n\t\t\t\trichText,\n\t\t\t})\n\t\t\treturn { output: {} }\n\t\t},\n\t}) as TaskConfig\n\n/** Register the actions task on `config.jobs.tasks`, creating the jobs config if absent. */\nexport const registerActionsTask = (\n\tconfig: Config,\n\tregistry: ActionRegistry,\n\trichText?: RichTextBodyOption\n): void => {\n\tconfig.jobs ??= {}\n\tconfig.jobs.tasks ??= []\n\tconfig.jobs.tasks.push(buildActionsTask(registry, richText))\n}\n"],"mappings":";;;;;AAWA,MAAa,oBAAoB;AAKjC,MAAM,aAAa,UAClB,MAAM,QAAQ,KAAK,IAAK,QAA6B,CAAC;AAEvD,MAAM,YAAY,UACjB,MAAM,QAAQ,KAAK,IAAK,QAA8B,CAAC;AAExD,MAAM,iBAAiB,UACtB,MAAM,QAAQ,KAAK,IAAK,QAAmC,CAAC;;;;;;AAO7D,MAAa,0BAA0B,OAAO,SAMzB;CACpB,MAAM,EAAE,OAAO,UAAU,SAAS,KAAK,aAAa;CACpD,MAAM,aAAa,MAAM,QACvB,SAAS;EACT,YAAY;EACZ,IAAI,MAAM;EACV,OAAO;EACP,gBAAgB;EAChB;CACD,CAAC,EACA,YAAY,IAAI;CAClB,IAAI,CAAC,YACJ;CAOD,MAAM,SAAS,OAAO,WAAW,WAAW,WAAW,WAAW,SAAU,KAAK,UAAU;CAE3F,MAAM,OAAO,MAAM,QACjB,SAAS;EACT,YAAY;EACZ,IAAI,MAAM;EACV,OAAO;EACP,gBAAgB;EAIR;EACR;CACD,CAAC,EACA,YAAY,IAAI;CAClB,IAAI,CAAC,MACJ;CAGD,MAAM,IAAe,iBAAiB,KAAK,MAAM,OAAO,QAAgB,IAAI;CAE5E,MAAM,WAAW;EAChB,SAAS,UAAU,KAAK,OAAO;EAC/B;EACA;EACA,MAAM;GAAE,IAAI,KAAK;GAAI,OAAO,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ,KAAA;EAAU;EACpF,cAAc,WAAW;EACzB,QAAQ,SAAS,WAAW,MAAM;EAClC,aAAa,cAAc,WAAW,WAAW;EACjD;EACA;EACA;EACA;CACD,CAAC;AACF;;AAGA,MAAa,oBACZ,UACA,cAEC;CACA,MAAM;CACN,aAAa,CACZ;EAAE,MAAM;EAAU,MAAM;EAAQ,UAAU;CAAK,GAC/C;EAAE,MAAM;EAAgB,MAAM;EAAQ,UAAU;CAAK,CACtD;CACA,SAAS,OAAO,EAAE,OAAO,UAAU;EAClC,MAAM,wBAAwB;GACtB;GACP;GACA,SAAS,IAAI;GACb;GACA;EACD,CAAC;EACD,OAAO,EAAE,QAAQ,CAAC,EAAE;CACrB;AACD;;AAGD,MAAa,uBACZ,QACA,UACA,aACU;CACV,OAAO,SAAS,CAAC;CACjB,OAAO,KAAK,UAAU,CAAC;CACvB,OAAO,KAAK,MAAM,KAAK,iBAAiB,UAAU,QAAQ,CAAC;AAC5D"}
|
|
@@ -11,6 +11,15 @@ type AggregateFormResponsesArgs = {
|
|
|
11
11
|
req?: PayloadRequest; /** Safety cap on submissions scanned; beyond it the result is flagged `truncated`. Default 10000. */
|
|
12
12
|
maxSubmissions?: number; /** Page size for the submission scan. Default 500. */
|
|
13
13
|
pageSize?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Source-resolved options by field name (an option-source poll's results field). They replace
|
|
16
|
+
* the instance's authored options for bucket seeding, order, and labels, and mark the field
|
|
17
|
+
* enumerable for the default field selection.
|
|
18
|
+
*/
|
|
19
|
+
resolvedOptions?: Record<string, {
|
|
20
|
+
value: string;
|
|
21
|
+
label: string;
|
|
22
|
+
}[]>;
|
|
14
23
|
};
|
|
15
24
|
/** True when a field declares non-empty options (a choice field safe to aggregate publicly). */
|
|
16
25
|
declare const fieldHasOptions: (field: FormFieldInstance) => boolean;
|
|
@@ -1,22 +1,16 @@
|
|
|
1
|
+
import { isNamedField } from "../fields/fieldKey.js";
|
|
2
|
+
import { instanceOptionsOf } from "../fields/instanceOptions.js";
|
|
1
3
|
import { aggregateRowsForFields } from "./aggregateRows.js";
|
|
2
4
|
import { FORMS_SLUG } from "../collections/forms.js";
|
|
3
5
|
import { FORM_SUBMISSIONS_SLUG } from "../collections/formSubmissions.js";
|
|
4
6
|
//#region src/aggregation/aggregateResponses.ts
|
|
5
|
-
const optionsOf = (field) => {
|
|
6
|
-
if (!Array.isArray(field.options)) return;
|
|
7
|
-
const options = field.options.filter((option) => typeof option?.value === "string").map((option) => ({
|
|
8
|
-
value: String(option.value),
|
|
9
|
-
label: option.label ?? String(option.value)
|
|
10
|
-
}));
|
|
11
|
-
return options.length > 0 ? options : void 0;
|
|
12
|
-
};
|
|
13
7
|
/** True when a field declares non-empty options (a choice field safe to aggregate publicly). */
|
|
14
|
-
const fieldHasOptions = (field) =>
|
|
15
|
-
const metaFor = (field) => ({
|
|
8
|
+
const fieldHasOptions = (field) => instanceOptionsOf(field) !== void 0;
|
|
9
|
+
const metaFor = (field, resolved) => ({
|
|
16
10
|
field: field.name,
|
|
17
11
|
label: field.label ?? field.name,
|
|
18
12
|
fieldType: field.blockType,
|
|
19
|
-
options:
|
|
13
|
+
options: resolved && resolved.length > 0 ? resolved : instanceOptionsOf(field)
|
|
20
14
|
});
|
|
21
15
|
/**
|
|
22
16
|
* Aggregate submission responses for a form. Loads the form for current labels + option order, then pages
|
|
@@ -25,7 +19,7 @@ const metaFor = (field) => ({
|
|
|
25
19
|
* `truncated`. Returns one `FieldAggregation` per requested field (or per enumerable field by default).
|
|
26
20
|
*/
|
|
27
21
|
const aggregateFormResponses = async (args) => {
|
|
28
|
-
const { payload, formId, fields, status = "complete", req, maxSubmissions = 1e4, pageSize = 500 } = args;
|
|
22
|
+
const { payload, formId, fields, status = "complete", req, maxSubmissions = 1e4, pageSize = 500, resolvedOptions } = args;
|
|
29
23
|
const form = await payload.findByID({
|
|
30
24
|
collection: FORMS_SLUG,
|
|
31
25
|
id: formId,
|
|
@@ -35,9 +29,9 @@ const aggregateFormResponses = async (args) => {
|
|
|
35
29
|
}).catch(() => null);
|
|
36
30
|
if (!form || !Array.isArray(form.fields)) return [];
|
|
37
31
|
const instances = form.fields;
|
|
38
|
-
const selected = fields ? instances.filter((field) => fields.includes(field.name)) : instances.filter((field) =>
|
|
32
|
+
const selected = fields ? instances.filter((field) => isNamedField(field) && fields.includes(field.name)) : instances.filter((field) => isNamedField(field) && (instanceOptionsOf(field) !== void 0 || (resolvedOptions?.[field.name]?.length ?? 0) > 0));
|
|
39
33
|
if (selected.length === 0) return [];
|
|
40
|
-
const metas = selected.map(metaFor);
|
|
34
|
+
const metas = selected.map((field) => metaFor(field, resolvedOptions?.[field.name]));
|
|
41
35
|
const where = { and: [{ form: { equals: formId } }, ...status === "all" ? [] : [{ status: { equals: status } }]] };
|
|
42
36
|
const rows = [];
|
|
43
37
|
let page = 1;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aggregateResponses.js","names":[],"sources":["../../src/aggregation/aggregateResponses.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { FORM_SUBMISSIONS_SLUG } from '../collections/formSubmissions'\nimport { FORMS_SLUG } from '../collections/forms'\nimport type { FormFieldInstance } from '../submissions/types'\nimport { aggregateRowsForFields } from './aggregateRows'\nimport type { AggregationRow, FieldAggregation, FieldMeta, SubmissionStatusFilter } from './types'\n\nexport type AggregateFormResponsesArgs = {\n\tpayload: Payload\n\tformId: number | string\n\t/** Field machine names to aggregate. Defaults to every field that declares options (enumerable). */\n\tfields?: string[]\n\t/** Submission status to count. Defaults to `complete`. */\n\tstatus?: SubmissionStatusFilter\n\treq?: PayloadRequest\n\t/** Safety cap on submissions scanned; beyond it the result is flagged `truncated`. Default 10000. */\n\tmaxSubmissions?: number\n\t/** Page size for the submission scan. Default 500. */\n\tpageSize?: number\n
|
|
1
|
+
{"version":3,"file":"aggregateResponses.js","names":[],"sources":["../../src/aggregation/aggregateResponses.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { FORM_SUBMISSIONS_SLUG } from '../collections/formSubmissions'\nimport { FORMS_SLUG } from '../collections/forms'\nimport { isNamedField, type NamedFormFieldInstance } from '../fields/fieldKey'\nimport { instanceOptionsOf } from '../fields/instanceOptions'\nimport type { FormFieldInstance } from '../submissions/types'\nimport { aggregateRowsForFields } from './aggregateRows'\nimport type { AggregationRow, FieldAggregation, FieldMeta, SubmissionStatusFilter } from './types'\n\nexport type AggregateFormResponsesArgs = {\n\tpayload: Payload\n\tformId: number | string\n\t/** Field machine names to aggregate. Defaults to every field that declares options (enumerable). */\n\tfields?: string[]\n\t/** Submission status to count. Defaults to `complete`. */\n\tstatus?: SubmissionStatusFilter\n\treq?: PayloadRequest\n\t/** Safety cap on submissions scanned; beyond it the result is flagged `truncated`. Default 10000. */\n\tmaxSubmissions?: number\n\t/** Page size for the submission scan. Default 500. */\n\tpageSize?: number\n\t/**\n\t * Source-resolved options by field name (an option-source poll's results field). They replace\n\t * the instance's authored options for bucket seeding, order, and labels, and mark the field\n\t * enumerable for the default field selection.\n\t */\n\tresolvedOptions?: Record<string, { value: string; label: string }[]>\n}\n\n/** True when a field declares non-empty options (a choice field safe to aggregate publicly). */\nexport const fieldHasOptions = (field: FormFieldInstance): boolean =>\n\tinstanceOptionsOf(field) !== undefined\n\nconst metaFor = (\n\tfield: NamedFormFieldInstance,\n\tresolved?: { value: string; label: string }[]\n): FieldMeta => ({\n\tfield: field.name,\n\tlabel: field.label ?? field.name,\n\tfieldType: field.blockType,\n\toptions: resolved && resolved.length > 0 ? resolved : instanceOptionsOf(field),\n})\n\n/**\n * Aggregate submission responses for a form. Loads the form for current labels + option order, then pages\n * `payload.find` over its submissions (JSON `values` cannot be GROUP BY'd portably across Mongo + Postgres,\n * so we scan + reduce in JS, cross-DB-identically). Bounded by `maxSubmissions`; beyond it the result is\n * `truncated`. Returns one `FieldAggregation` per requested field (or per enumerable field by default).\n */\nexport const aggregateFormResponses = async (\n\targs: AggregateFormResponsesArgs\n): Promise<FieldAggregation[]> => {\n\tconst {\n\t\tpayload,\n\t\tformId,\n\t\tfields,\n\t\tstatus = 'complete',\n\t\treq,\n\t\tmaxSubmissions = 10000,\n\t\tpageSize = 500,\n\t\tresolvedOptions,\n\t} = args\n\tconst form = await payload\n\t\t.findByID({ collection: FORMS_SLUG, id: formId, depth: 0, overrideAccess: true, req })\n\t\t.catch(() => null)\n\tif (!form || !Array.isArray(form.fields)) {\n\t\treturn []\n\t}\n\tconst instances = form.fields as FormFieldInstance[]\n\tconst selected = fields\n\t\t? instances.filter(\n\t\t\t\t(field): field is NamedFormFieldInstance =>\n\t\t\t\t\tisNamedField(field) && fields.includes(field.name)\n\t\t\t)\n\t\t: instances.filter(\n\t\t\t\t(field): field is NamedFormFieldInstance =>\n\t\t\t\t\tisNamedField(field) &&\n\t\t\t\t\t(instanceOptionsOf(field) !== undefined ||\n\t\t\t\t\t\t(resolvedOptions?.[field.name]?.length ?? 0) > 0)\n\t\t\t)\n\tif (selected.length === 0) {\n\t\treturn []\n\t}\n\tconst metas = selected.map((field) => metaFor(field, resolvedOptions?.[field.name]))\n\n\tconst statusWhere = status === 'all' ? [] : [{ status: { equals: status } }]\n\tconst where = { and: [{ form: { equals: formId } }, ...statusWhere] }\n\n\tconst rows: AggregationRow[] = []\n\tlet page = 1\n\tlet truncated = false\n\tfor (;;) {\n\t\tconst result = await payload.find({\n\t\t\tcollection: FORM_SUBMISSIONS_SLUG,\n\t\t\twhere,\n\t\t\tlimit: pageSize,\n\t\t\tpage,\n\t\t\tdepth: 0,\n\t\t\toverrideAccess: true,\n\t\t\treq,\n\t\t\tselect: { values: true, descriptors: true },\n\t\t})\n\t\tfor (const doc of result.docs as AggregationRow[]) {\n\t\t\t// Check before pushing so `truncated` means \"matching rows were left out\", not \"the cap was reached\n\t\t\t// exactly\": a form with precisely `maxSubmissions` responses is complete, not a sample.\n\t\t\tif (rows.length >= maxSubmissions) {\n\t\t\t\ttruncated = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t\trows.push({ values: doc.values, descriptors: doc.descriptors })\n\t\t}\n\t\tif (truncated || !result.hasNextPage) {\n\t\t\tbreak\n\t\t}\n\t\tpage += 1\n\t}\n\n\treturn aggregateRowsForFields(rows, metas, truncated)\n}\n\nexport type AggregateFieldResponsesArgs = Omit<AggregateFormResponsesArgs, 'fields'> & {\n\tfield: string\n}\n\n/** Single-field convenience over `aggregateFormResponses`. Returns the field's aggregation, or null if absent. */\nexport const aggregateFieldResponses = async (\n\targs: AggregateFieldResponsesArgs\n): Promise<FieldAggregation | null> => {\n\tconst { field, ...rest } = args\n\tconst [result] = await aggregateFormResponses({ ...rest, fields: [field] })\n\treturn result ?? null\n}\n"],"mappings":";;;;;;;AA8BA,MAAa,mBAAmB,UAC/B,kBAAkB,KAAK,MAAM,KAAA;AAE9B,MAAM,WACL,OACA,cACgB;CAChB,OAAO,MAAM;CACb,OAAO,MAAM,SAAS,MAAM;CAC5B,WAAW,MAAM;CACjB,SAAS,YAAY,SAAS,SAAS,IAAI,WAAW,kBAAkB,KAAK;AAC9E;;;;;;;AAQA,MAAa,yBAAyB,OACrC,SACiC;CACjC,MAAM,EACL,SACA,QACA,QACA,SAAS,YACT,KACA,iBAAiB,KACjB,WAAW,KACX,oBACG;CACJ,MAAM,OAAO,MAAM,QACjB,SAAS;EAAE,YAAY;EAAY,IAAI;EAAQ,OAAO;EAAG,gBAAgB;EAAM;CAAI,CAAC,EACpF,YAAY,IAAI;CAClB,IAAI,CAAC,QAAQ,CAAC,MAAM,QAAQ,KAAK,MAAM,GACtC,OAAO,CAAC;CAET,MAAM,YAAY,KAAK;CACvB,MAAM,WAAW,SACd,UAAU,QACT,UACA,aAAa,KAAK,KAAK,OAAO,SAAS,MAAM,IAAI,CACnD,IACC,UAAU,QACT,UACA,aAAa,KAAK,MACjB,kBAAkB,KAAK,MAAM,KAAA,MAC5B,kBAAkB,MAAM,OAAO,UAAU,KAAK,EAClD;CACF,IAAI,SAAS,WAAW,GACvB,OAAO,CAAC;CAET,MAAM,QAAQ,SAAS,KAAK,UAAU,QAAQ,OAAO,kBAAkB,MAAM,KAAK,CAAC;CAGnF,MAAM,QAAQ,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,QAAQ,OAAO,EAAE,GAAG,GADhC,WAAW,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,QAAQ,OAAO,EAAE,CAAC,CACT,EAAE;CAEpE,MAAM,OAAyB,CAAC;CAChC,IAAI,OAAO;CACX,IAAI,YAAY;CAChB,SAAS;EACR,MAAM,SAAS,MAAM,QAAQ,KAAK;GACjC,YAAY;GACZ;GACA,OAAO;GACP;GACA,OAAO;GACP,gBAAgB;GAChB;GACA,QAAQ;IAAE,QAAQ;IAAM,aAAa;GAAK;EAC3C,CAAC;EACD,KAAK,MAAM,OAAO,OAAO,MAA0B;GAGlD,IAAI,KAAK,UAAU,gBAAgB;IAClC,YAAY;IACZ;GACD;GACA,KAAK,KAAK;IAAE,QAAQ,IAAI;IAAQ,aAAa,IAAI;GAAY,CAAC;EAC/D;EACA,IAAI,aAAa,CAAC,OAAO,aACxB;EAED,QAAQ;CACT;CAEA,OAAO,uBAAuB,MAAM,OAAO,SAAS;AACrD;;AAOA,MAAa,0BAA0B,OACtC,SACsC;CACtC,MAAM,EAAE,OAAO,GAAG,SAAS;CAC3B,MAAM,CAAC,UAAU,MAAM,uBAAuB;EAAE,GAAG;EAAM,QAAQ,CAAC,KAAK;CAAE,CAAC;CAC1E,OAAO,UAAU;AAClB"}
|
|
@@ -22,6 +22,13 @@ type ResolveResultsRequestArgs = {
|
|
|
22
22
|
isAuthed: boolean;
|
|
23
23
|
req?: PayloadRequest; /** Optional host seam for anonymous reads; absent means plugin-default gating only. */
|
|
24
24
|
access?: FormResultsAccess;
|
|
25
|
+
/**
|
|
26
|
+
* Poll-eligible field types (registry-derived). When set, an anonymous read of an
|
|
27
|
+
* option-source poll also requires the results field's instance to be one of these types,
|
|
28
|
+
* so legacy or db-written docs pointing at a free-text field can never expose stored
|
|
29
|
+
* answers as leftover result buckets.
|
|
30
|
+
*/
|
|
31
|
+
eligibleTypes?: readonly string[];
|
|
25
32
|
};
|
|
26
33
|
type ResolveResultsRequestResult = {
|
|
27
34
|
status: number;
|
|
@@ -35,12 +42,18 @@ type ResolveResultsRequestResult = {
|
|
|
35
42
|
};
|
|
36
43
|
/**
|
|
37
44
|
* Authorize and resolve a poll/survey results request. Authed callers may aggregate any field (or all
|
|
38
|
-
* enumerable fields) and bypass the `access` seam
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
45
|
+
* enumerable fields) and bypass the `access` seam; for a poll-enabled form they also get the results
|
|
46
|
+
* field's effective options injected so sourced/resolver-backed polls render labels (best-effort, a
|
|
47
|
+
* resolve failure degrades to raw values rather than blocking the trusted read). Anonymous callers are
|
|
48
|
+
* allowed only when the form's poll is enabled, the poll's `resultsVisibility` permits it (`afterVote`:
|
|
49
|
+
* any time; `afterClose`: only once `closesAt` has passed), and the optional host `access` seam
|
|
50
|
+
* approves; and then only for the configured `poll.resultsField`, and only if that field is enumerable
|
|
51
|
+
* so a misconfigured `resultsField` pointing at a free-text or PII field can never be dumped publicly. A
|
|
52
|
+
* static poll is enumerable through its authored options; a poll whose options come from an
|
|
53
|
+
* `optionSource` or the field type's own `resolveOptions` resolves them here (registry via
|
|
54
|
+
* `config.custom`, per-request cache), gated by `eligibleTypes` and enumerable only when resolution
|
|
55
|
+
* yields any, with the resolved options driving bucket order and labels. Resolution failure fails
|
|
56
|
+
* closed (503) on the anonymous path. Returns only aggregate counts, never raw submissions.
|
|
44
57
|
*/
|
|
45
58
|
declare const resolveFormResultsRequest: (args: ResolveResultsRequestArgs) => Promise<ResolveResultsRequestResult>;
|
|
46
59
|
//#endregion
|
|
@@ -1,22 +1,35 @@
|
|
|
1
1
|
import { isPollClosed, pollConfigOf } from "../form/pollState.js";
|
|
2
2
|
import { aggregateFormResponses, fieldHasOptions } from "./aggregateResponses.js";
|
|
3
|
+
import { resolvePollOutcome } from "../poll/resolvePollOutcome.js";
|
|
4
|
+
import { shouldAutoResolvePoll } from "../poll/closeJob.js";
|
|
5
|
+
import { resolveEffectivePollOptions } from "../poll/effectivePollOptions.js";
|
|
3
6
|
import { FORMS_SLUG } from "../collections/forms.js";
|
|
4
7
|
//#region src/aggregation/resolveResultsRequest.ts
|
|
5
8
|
const forbidden = {
|
|
6
9
|
status: 403,
|
|
7
10
|
body: { errors: [{ message: "Forbidden" }] }
|
|
8
11
|
};
|
|
12
|
+
const unavailable = {
|
|
13
|
+
status: 503,
|
|
14
|
+
body: { errors: [{ message: "Poll options unavailable" }] }
|
|
15
|
+
};
|
|
9
16
|
/**
|
|
10
17
|
* Authorize and resolve a poll/survey results request. Authed callers may aggregate any field (or all
|
|
11
|
-
* enumerable fields) and bypass the `access` seam
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
18
|
+
* enumerable fields) and bypass the `access` seam; for a poll-enabled form they also get the results
|
|
19
|
+
* field's effective options injected so sourced/resolver-backed polls render labels (best-effort, a
|
|
20
|
+
* resolve failure degrades to raw values rather than blocking the trusted read). Anonymous callers are
|
|
21
|
+
* allowed only when the form's poll is enabled, the poll's `resultsVisibility` permits it (`afterVote`:
|
|
22
|
+
* any time; `afterClose`: only once `closesAt` has passed), and the optional host `access` seam
|
|
23
|
+
* approves; and then only for the configured `poll.resultsField`, and only if that field is enumerable
|
|
24
|
+
* so a misconfigured `resultsField` pointing at a free-text or PII field can never be dumped publicly. A
|
|
25
|
+
* static poll is enumerable through its authored options; a poll whose options come from an
|
|
26
|
+
* `optionSource` or the field type's own `resolveOptions` resolves them here (registry via
|
|
27
|
+
* `config.custom`, per-request cache), gated by `eligibleTypes` and enumerable only when resolution
|
|
28
|
+
* yields any, with the resolved options driving bucket order and labels. Resolution failure fails
|
|
29
|
+
* closed (503) on the anonymous path. Returns only aggregate counts, never raw submissions.
|
|
17
30
|
*/
|
|
18
31
|
const resolveFormResultsRequest = async (args) => {
|
|
19
|
-
const { payload, formId, field, isAuthed, req, access } = args;
|
|
32
|
+
const { payload, formId, field, isAuthed, req, access, eligibleTypes } = args;
|
|
20
33
|
if (formId == null) return {
|
|
21
34
|
status: 400,
|
|
22
35
|
body: { errors: [{ message: "Missing form id" }] }
|
|
@@ -32,11 +45,28 @@ const resolveFormResultsRequest = async (args) => {
|
|
|
32
45
|
status: 404,
|
|
33
46
|
body: { errors: [{ message: "Not found" }] }
|
|
34
47
|
};
|
|
48
|
+
if (shouldAutoResolvePoll(form)) await resolvePollOutcome({
|
|
49
|
+
payload,
|
|
50
|
+
formId,
|
|
51
|
+
req
|
|
52
|
+
}).catch(() => void 0);
|
|
35
53
|
let fields;
|
|
36
|
-
|
|
37
|
-
|
|
54
|
+
let resolvedOptions;
|
|
55
|
+
if (isAuthed) {
|
|
56
|
+
fields = field ? [field] : void 0;
|
|
38
57
|
const poll = pollConfigOf(form.poll);
|
|
39
|
-
|
|
58
|
+
const resultsField = typeof poll?.resultsField === "string" && poll.resultsField.length > 0 ? poll.resultsField : void 0;
|
|
59
|
+
if (form.pollEnabled === true && resultsField && (!field || field === resultsField)) {
|
|
60
|
+
const options = await resolveEffectivePollOptions({
|
|
61
|
+
payload,
|
|
62
|
+
req,
|
|
63
|
+
form
|
|
64
|
+
}).catch(() => []);
|
|
65
|
+
if (options.length > 0) resolvedOptions = { [resultsField]: options };
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
const poll = pollConfigOf(form.poll);
|
|
69
|
+
if (form.pollEnabled !== true || !poll) return forbidden;
|
|
40
70
|
if (poll.resultsVisibility === "afterClose" && !isPollClosed(poll)) return forbidden;
|
|
41
71
|
if (access) {
|
|
42
72
|
if (!(req ? await access({
|
|
@@ -48,7 +78,22 @@ const resolveFormResultsRequest = async (args) => {
|
|
|
48
78
|
if (!publicField) return forbidden;
|
|
49
79
|
if (field && field !== publicField) return forbidden;
|
|
50
80
|
const instance = (Array.isArray(form.fields) ? form.fields : []).find((entry) => entry.name === publicField);
|
|
51
|
-
if (!instance
|
|
81
|
+
if (!instance) return forbidden;
|
|
82
|
+
const hasSource = typeof poll.optionSource === "string" && poll.optionSource.length > 0;
|
|
83
|
+
const authored = fieldHasOptions(instance);
|
|
84
|
+
if ((hasSource || !authored) && eligibleTypes && !eligibleTypes.includes(instance.blockType)) return forbidden;
|
|
85
|
+
let options;
|
|
86
|
+
try {
|
|
87
|
+
options = await resolveEffectivePollOptions({
|
|
88
|
+
payload,
|
|
89
|
+
req,
|
|
90
|
+
form
|
|
91
|
+
});
|
|
92
|
+
} catch {
|
|
93
|
+
return unavailable;
|
|
94
|
+
}
|
|
95
|
+
if (options.length === 0) return forbidden;
|
|
96
|
+
resolvedOptions = { [publicField]: options };
|
|
52
97
|
fields = [publicField];
|
|
53
98
|
}
|
|
54
99
|
return {
|
|
@@ -57,7 +102,8 @@ const resolveFormResultsRequest = async (args) => {
|
|
|
57
102
|
payload,
|
|
58
103
|
formId,
|
|
59
104
|
fields,
|
|
60
|
-
req
|
|
105
|
+
req,
|
|
106
|
+
resolvedOptions
|
|
61
107
|
}) }
|
|
62
108
|
};
|
|
63
109
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolveResultsRequest.js","names":[],"sources":["../../src/aggregation/resolveResultsRequest.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { FORMS_SLUG } from '../collections/forms'\nimport { isPollClosed, pollConfigOf } from '../form/pollState'\nimport type { FormFieldInstance } from '../submissions/types'\nimport { aggregateFormResponses, fieldHasOptions } from './aggregateResponses'\nimport type { FieldAggregation } from './types'\n\nexport type FormResultsAccessArgs = {\n\treq: PayloadRequest\n\t/** The loaded forms document (depth 0). Untyped beyond `id`: hosts read their own fields (e.g. `form.tenant`). */\n\tform: { id: number | string } & Record<string, unknown>\n}\n\n/**\n * Host seam gating anonymous results reads, evaluated after the form is loaded and before anything\n * is aggregated. Multi-tenant recipe: compare `form.tenant` against the tenant derived from `req`\n * (host header, cookie, or auth context) and return `false` for a cross-tenant read. Authenticated\n * callers bypass this seam (they are admin-trusted, like the rest of the results endpoint).\n */\nexport type FormResultsAccess = (args: FormResultsAccessArgs) => boolean | Promise<boolean>\n\nexport type ResolveResultsRequestArgs = {\n\tpayload: Payload\n\tformId: number | string | undefined\n\t/** The requested field (query param). */\n\tfield?: string\n\t/** Whether the caller is authenticated (an admin/user). */\n\tisAuthed: boolean\n\treq?: PayloadRequest\n\t/** Optional host seam for anonymous reads; absent means plugin-default gating only. */\n\taccess?: FormResultsAccess\n}\n\nexport type ResolveResultsRequestResult = {\n\tstatus: number\n\tbody: { results: FieldAggregation[] } | { errors: { message: string }[] }\n}\n\nconst forbidden: ResolveResultsRequestResult = {\n\tstatus: 403,\n\tbody: { errors: [{ message: 'Forbidden' }] },\n}\n\n/**\n * Authorize and resolve a poll/survey results request. Authed callers may aggregate any field (or all\n * enumerable fields) and bypass the `access` seam. Anonymous callers are allowed only when the form's\n * poll is enabled, the poll's `resultsVisibility` permits it (`afterVote`: any time; `afterClose`: only\n * once `closesAt` has passed), and the optional host `access` seam approves; and then only for the\n * configured `poll.resultsField`, and only if that field is enumerable (has options) so a misconfigured\n * `resultsField` pointing at a free-text or PII field can never be dumped publicly. Returns only\n * aggregate counts, never raw submissions.\n */\nexport const resolveFormResultsRequest = async (\n\targs: ResolveResultsRequestArgs\n): Promise<ResolveResultsRequestResult> => {\n\tconst { payload, formId, field, isAuthed, req, access } = args\n\tif (formId == null) {\n\t\treturn { status: 400, body: { errors: [{ message: 'Missing form id' }] } }\n\t}\n\tconst form = await payload\n\t\t.findByID({ collection: FORMS_SLUG, id: formId, depth: 0, overrideAccess: true, req })\n\t\t.catch(() => null)\n\tif (!form) {\n\t\treturn { status: 404, body: { errors: [{ message: 'Not found' }] } }\n\t}\n\n\tlet fields: string[] | undefined\n\tif (isAuthed) {\n\t\tfields = field ? [field] : undefined\n\t} else {\n\t\tconst poll = pollConfigOf(form.poll)\n\t\tif (poll?.enabled !== true) {\n\t\t\treturn forbidden\n\t\t}\n\t\tif (poll.resultsVisibility === 'afterClose' && !isPollClosed(poll)) {\n\t\t\treturn forbidden\n\t\t}\n\t\tif (access) {\n\t\t\t// No req means the seam cannot be evaluated; fail closed rather than skip a configured gate.\n\t\t\t// The concrete generated Form doc has no index signature; the seam's Record<string, unknown>\n\t\t\t// is the ergonomic host-facing contract, so widen the doc to it here.\n\t\t\tconst allowed = req\n\t\t\t\t? await access({ req, form: form as unknown as FormResultsAccessArgs['form'] })\n\t\t\t\t: false\n\t\t\tif (!allowed) {\n\t\t\t\treturn forbidden\n\t\t\t}\n\t\t}\n\t\tconst publicField =\n\t\t\ttypeof poll.resultsField === 'string' && poll.resultsField.length > 0\n\t\t\t\t? poll.resultsField\n\t\t\t\t: undefined\n\t\tif (!publicField) {\n\t\t\treturn forbidden\n\t\t}\n\t\tif (field && field !== publicField) {\n\t\t\treturn forbidden\n\t\t}\n\t\tconst instances = Array.isArray(form.fields) ? (form.fields as FormFieldInstance[]) : []\n\t\tconst instance = instances.find((entry) => entry.name === publicField)\n\t\tif (!instance || !fieldHasOptions(instance)) {\n\t\t\treturn forbidden\n\t\t}\n\t\tfields = [publicField]\n\t}\n\n\tconst results = await aggregateFormResponses({ payload, formId, fields, req })\n\treturn { status: 200, body: { results } }\n}\n"],"mappings":";;;;AAsCA,MAAM,YAAyC;CAC9C,QAAQ;CACR,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,EAAE;AAC5C;;;;;;;;;;AAWA,MAAa,4BAA4B,OACxC,SAC0C;CAC1C,MAAM,EAAE,SAAS,QAAQ,OAAO,UAAU,KAAK,WAAW;CAC1D,IAAI,UAAU,MACb,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,kBAAkB,CAAC,EAAE;CAAE;CAE1E,MAAM,OAAO,MAAM,QACjB,SAAS;EAAE,YAAY;EAAY,IAAI;EAAQ,OAAO;EAAG,gBAAgB;EAAM;CAAI,CAAC,EACpF,YAAY,IAAI;CAClB,IAAI,CAAC,MACJ,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,EAAE;CAAE;CAGpE,IAAI;CACJ,IAAI,UACH,SAAS,QAAQ,CAAC,KAAK,IAAI,KAAA;MACrB;EACN,MAAM,OAAO,aAAa,KAAK,IAAI;EACnC,IAAI,MAAM,YAAY,MACrB,OAAO;EAER,IAAI,KAAK,sBAAsB,gBAAgB,CAAC,aAAa,IAAI,GAChE,OAAO;EAER,IAAI;OAOC,EAHY,MACb,MAAM,OAAO;IAAE;IAAW;GAAiD,CAAC,IAC5E,QAEF,OAAO;EAAA;EAGT,MAAM,cACL,OAAO,KAAK,iBAAiB,YAAY,KAAK,aAAa,SAAS,IACjE,KAAK,eACL,KAAA;EACJ,IAAI,CAAC,aACJ,OAAO;EAER,IAAI,SAAS,UAAU,aACtB,OAAO;EAGR,MAAM,YADY,MAAM,QAAQ,KAAK,MAAM,IAAK,KAAK,SAAiC,CAAC,GAC5D,MAAM,UAAU,MAAM,SAAS,WAAW;EACrE,IAAI,CAAC,YAAY,CAAC,gBAAgB,QAAQ,GACzC,OAAO;EAER,SAAS,CAAC,WAAW;CACtB;CAGA,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,SAAA,MADR,uBAAuB;GAAE;GAAS;GAAQ;GAAQ;EAAI,CAAC,EACvC;CAAE;AACzC"}
|
|
1
|
+
{"version":3,"file":"resolveResultsRequest.js","names":[],"sources":["../../src/aggregation/resolveResultsRequest.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { FORMS_SLUG } from '../collections/forms'\nimport { isPollClosed, pollConfigOf } from '../form/pollState'\nimport { type PollFormLike, shouldAutoResolvePoll } from '../poll/closeJob'\nimport type { PollOption } from '../poll/definePollOptionSource'\nimport { resolveEffectivePollOptions } from '../poll/effectivePollOptions'\nimport { resolvePollOutcome } from '../poll/resolvePollOutcome'\nimport type { FormFieldInstance } from '../submissions/types'\nimport { aggregateFormResponses, fieldHasOptions } from './aggregateResponses'\nimport type { FieldAggregation } from './types'\n\nexport type FormResultsAccessArgs = {\n\treq: PayloadRequest\n\t/** The loaded forms document (depth 0). Untyped beyond `id`: hosts read their own fields (e.g. `form.tenant`). */\n\tform: { id: number | string } & Record<string, unknown>\n}\n\n/**\n * Host seam gating anonymous results reads, evaluated after the form is loaded and before anything\n * is aggregated. Multi-tenant recipe: compare `form.tenant` against the tenant derived from `req`\n * (host header, cookie, or auth context) and return `false` for a cross-tenant read. Authenticated\n * callers bypass this seam (they are admin-trusted, like the rest of the results endpoint).\n */\nexport type FormResultsAccess = (args: FormResultsAccessArgs) => boolean | Promise<boolean>\n\nexport type ResolveResultsRequestArgs = {\n\tpayload: Payload\n\tformId: number | string | undefined\n\t/** The requested field (query param). */\n\tfield?: string\n\t/** Whether the caller is authenticated (an admin/user). */\n\tisAuthed: boolean\n\treq?: PayloadRequest\n\t/** Optional host seam for anonymous reads; absent means plugin-default gating only. */\n\taccess?: FormResultsAccess\n\t/**\n\t * Poll-eligible field types (registry-derived). When set, an anonymous read of an\n\t * option-source poll also requires the results field's instance to be one of these types,\n\t * so legacy or db-written docs pointing at a free-text field can never expose stored\n\t * answers as leftover result buckets.\n\t */\n\teligibleTypes?: readonly string[]\n}\n\nexport type ResolveResultsRequestResult = {\n\tstatus: number\n\tbody: { results: FieldAggregation[] } | { errors: { message: string }[] }\n}\n\nconst forbidden: ResolveResultsRequestResult = {\n\tstatus: 403,\n\tbody: { errors: [{ message: 'Forbidden' }] },\n}\n\n// 503 over 403 for a failed option-source resolve: the form and its poll config are already\n// publicly readable, so \"temporarily unavailable\" leaks nothing new, matches the submission\n// path's precedent, and does not mislabel a transient server fault as an authorization denial.\nconst unavailable: ResolveResultsRequestResult = {\n\tstatus: 503,\n\tbody: { errors: [{ message: 'Poll options unavailable' }] },\n}\n\n/**\n * Authorize and resolve a poll/survey results request. Authed callers may aggregate any field (or all\n * enumerable fields) and bypass the `access` seam; for a poll-enabled form they also get the results\n * field's effective options injected so sourced/resolver-backed polls render labels (best-effort, a\n * resolve failure degrades to raw values rather than blocking the trusted read). Anonymous callers are\n * allowed only when the form's poll is enabled, the poll's `resultsVisibility` permits it (`afterVote`:\n * any time; `afterClose`: only once `closesAt` has passed), and the optional host `access` seam\n * approves; and then only for the configured `poll.resultsField`, and only if that field is enumerable\n * so a misconfigured `resultsField` pointing at a free-text or PII field can never be dumped publicly. A\n * static poll is enumerable through its authored options; a poll whose options come from an\n * `optionSource` or the field type's own `resolveOptions` resolves them here (registry via\n * `config.custom`, per-request cache), gated by `eligibleTypes` and enumerable only when resolution\n * yields any, with the resolved options driving bucket order and labels. Resolution failure fails\n * closed (503) on the anonymous path. Returns only aggregate counts, never raw submissions.\n */\nexport const resolveFormResultsRequest = async (\n\targs: ResolveResultsRequestArgs\n): Promise<ResolveResultsRequestResult> => {\n\tconst { payload, formId, field, isAuthed, req, access, eligibleTypes } = args\n\tif (formId == null) {\n\t\treturn { status: 400, body: { errors: [{ message: 'Missing form id' }] } }\n\t}\n\tconst form = await payload\n\t\t.findByID({ collection: FORMS_SLUG, id: formId, depth: 0, overrideAccess: true, req })\n\t\t.catch(() => null)\n\tif (!form) {\n\t\treturn { status: 404, body: { errors: [{ message: 'Not found' }] } }\n\t}\n\n\t// No-runner safety net: when a closed poll's `mostVoted`/`source` outcome was never auto-resolved\n\t// (no wired job runner fired the close task), a results read heals it. Idempotent and gate-neutral,\n\t// it runs on the already-loaded form via overrideAccess and writes only through the outcome hook, so\n\t// it cannot relax the anonymous authorization below. A failure degrades silently to the normal path.\n\tif (shouldAutoResolvePoll(form as PollFormLike)) {\n\t\tawait resolvePollOutcome({ payload, formId, req }).catch(() => undefined)\n\t}\n\n\tlet fields: string[] | undefined\n\tlet resolvedOptions: Record<string, { value: string; label: string }[]> | undefined\n\tif (isAuthed) {\n\t\tfields = field ? [field] : undefined\n\t\t// Label a poll's results field for admins: resolve its effective options (a source, the\n\t\t// field type's own resolver, or authored options) and inject them so a sourced/resolved poll\n\t\t// shows labels instead of raw stored values, and so an all-fields read still surfaces a\n\t\t// resolver-backed field that carries no authored options. Best-effort: a resolver/source\n\t\t// failure degrades to raw values rather than blocking the trusted read (the anonymous path\n\t\t// below fails closed instead). Only run when the read would actually surface the results field.\n\t\tconst poll = pollConfigOf(form.poll)\n\t\tconst resultsField =\n\t\t\ttypeof poll?.resultsField === 'string' && poll.resultsField.length > 0\n\t\t\t\t? poll.resultsField\n\t\t\t\t: undefined\n\t\tif (form.pollEnabled === true && resultsField && (!field || field === resultsField)) {\n\t\t\tconst options: PollOption[] = await resolveEffectivePollOptions({\n\t\t\t\tpayload,\n\t\t\t\treq,\n\t\t\t\tform,\n\t\t\t}).catch(() => [])\n\t\t\tif (options.length > 0) {\n\t\t\t\tresolvedOptions = { [resultsField]: options }\n\t\t\t}\n\t\t}\n\t} else {\n\t\tconst poll = pollConfigOf(form.poll)\n\t\tif (form.pollEnabled !== true || !poll) {\n\t\t\treturn forbidden\n\t\t}\n\t\tif (poll.resultsVisibility === 'afterClose' && !isPollClosed(poll)) {\n\t\t\treturn forbidden\n\t\t}\n\t\tif (access) {\n\t\t\t// No req means the seam cannot be evaluated; fail closed rather than skip a configured gate.\n\t\t\t// The concrete generated Form doc has no index signature; the seam's Record<string, unknown>\n\t\t\t// is the ergonomic host-facing contract, so widen the doc to it here.\n\t\t\tconst allowed = req\n\t\t\t\t? await access({ req, form: form as unknown as FormResultsAccessArgs['form'] })\n\t\t\t\t: false\n\t\t\tif (!allowed) {\n\t\t\t\treturn forbidden\n\t\t\t}\n\t\t}\n\t\tconst publicField =\n\t\t\ttypeof poll.resultsField === 'string' && poll.resultsField.length > 0\n\t\t\t\t? poll.resultsField\n\t\t\t\t: undefined\n\t\tif (!publicField) {\n\t\t\treturn forbidden\n\t\t}\n\t\tif (field && field !== publicField) {\n\t\t\treturn forbidden\n\t\t}\n\t\tconst instances = Array.isArray(form.fields) ? (form.fields as FormFieldInstance[]) : []\n\t\tconst instance = instances.find((entry) => entry.name === publicField)\n\t\tif (!instance) {\n\t\t\treturn forbidden\n\t\t}\n\t\tconst hasSource = typeof poll.optionSource === 'string' && poll.optionSource.length > 0\n\t\tconst authored = fieldHasOptions(instance)\n\t\t// Options that don't come from the author's own literal `options` list (a source or a\n\t\t// field-typed resolver) require the results field to be a poll-eligible type, so a legacy or\n\t\t// db-written doc pointing `resultsField` at a free-text field can never dump raw answers as\n\t\t// leftover buckets. A static authored poll is a choice field already, so it keeps serving.\n\t\tif ((hasSource || !authored) && eligibleTypes && !eligibleTypes.includes(instance.blockType)) {\n\t\t\treturn forbidden\n\t\t}\n\t\tlet options: PollOption[]\n\t\ttry {\n\t\t\toptions = await resolveEffectivePollOptions({ payload, req, form })\n\t\t} catch {\n\t\t\treturn unavailable\n\t\t}\n\t\tif (options.length === 0) {\n\t\t\treturn forbidden\n\t\t}\n\t\tresolvedOptions = { [publicField]: options }\n\t\tfields = [publicField]\n\t}\n\n\tconst results = await aggregateFormResponses({ payload, formId, fields, req, resolvedOptions })\n\treturn { status: 200, body: { results } }\n}\n"],"mappings":";;;;;;;AAiDA,MAAM,YAAyC;CAC9C,QAAQ;CACR,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,EAAE;AAC5C;AAKA,MAAM,cAA2C;CAChD,QAAQ;CACR,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,2BAA2B,CAAC,EAAE;AAC3D;;;;;;;;;;;;;;;;AAiBA,MAAa,4BAA4B,OACxC,SAC0C;CAC1C,MAAM,EAAE,SAAS,QAAQ,OAAO,UAAU,KAAK,QAAQ,kBAAkB;CACzE,IAAI,UAAU,MACb,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,kBAAkB,CAAC,EAAE;CAAE;CAE1E,MAAM,OAAO,MAAM,QACjB,SAAS;EAAE,YAAY;EAAY,IAAI;EAAQ,OAAO;EAAG,gBAAgB;EAAM;CAAI,CAAC,EACpF,YAAY,IAAI;CAClB,IAAI,CAAC,MACJ,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,EAAE;CAAE;CAOpE,IAAI,sBAAsB,IAAoB,GAC7C,MAAM,mBAAmB;EAAE;EAAS;EAAQ;CAAI,CAAC,EAAE,YAAY,KAAA,CAAS;CAGzE,IAAI;CACJ,IAAI;CACJ,IAAI,UAAU;EACb,SAAS,QAAQ,CAAC,KAAK,IAAI,KAAA;EAO3B,MAAM,OAAO,aAAa,KAAK,IAAI;EACnC,MAAM,eACL,OAAO,MAAM,iBAAiB,YAAY,KAAK,aAAa,SAAS,IAClE,KAAK,eACL,KAAA;EACJ,IAAI,KAAK,gBAAgB,QAAQ,iBAAiB,CAAC,SAAS,UAAU,eAAe;GACpF,MAAM,UAAwB,MAAM,4BAA4B;IAC/D;IACA;IACA;GACD,CAAC,EAAE,YAAY,CAAC,CAAC;GACjB,IAAI,QAAQ,SAAS,GACpB,kBAAkB,GAAG,eAAe,QAAQ;EAE9C;CACD,OAAO;EACN,MAAM,OAAO,aAAa,KAAK,IAAI;EACnC,IAAI,KAAK,gBAAgB,QAAQ,CAAC,MACjC,OAAO;EAER,IAAI,KAAK,sBAAsB,gBAAgB,CAAC,aAAa,IAAI,GAChE,OAAO;EAER,IAAI;OAOC,EAHY,MACb,MAAM,OAAO;IAAE;IAAW;GAAiD,CAAC,IAC5E,QAEF,OAAO;EAAA;EAGT,MAAM,cACL,OAAO,KAAK,iBAAiB,YAAY,KAAK,aAAa,SAAS,IACjE,KAAK,eACL,KAAA;EACJ,IAAI,CAAC,aACJ,OAAO;EAER,IAAI,SAAS,UAAU,aACtB,OAAO;EAGR,MAAM,YADY,MAAM,QAAQ,KAAK,MAAM,IAAK,KAAK,SAAiC,CAAC,GAC5D,MAAM,UAAU,MAAM,SAAS,WAAW;EACrE,IAAI,CAAC,UACJ,OAAO;EAER,MAAM,YAAY,OAAO,KAAK,iBAAiB,YAAY,KAAK,aAAa,SAAS;EACtF,MAAM,WAAW,gBAAgB,QAAQ;EAKzC,KAAK,aAAa,CAAC,aAAa,iBAAiB,CAAC,cAAc,SAAS,SAAS,SAAS,GAC1F,OAAO;EAER,IAAI;EACJ,IAAI;GACH,UAAU,MAAM,4BAA4B;IAAE;IAAS;IAAK;GAAK,CAAC;EACnE,QAAQ;GACP,OAAO;EACR;EACA,IAAI,QAAQ,WAAW,GACtB,OAAO;EAER,kBAAkB,GAAG,cAAc,QAAQ;EAC3C,SAAS,CAAC,WAAW;CACtB;CAGA,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,SAAA,MADR,uBAAuB;GAAE;GAAS;GAAQ;GAAQ;GAAK;EAAgB,CAAC,EACxD;CAAE;AACzC"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isNamedField } from "../fields/fieldKey.js";
|
|
1
2
|
import { evaluateCalc } from "./evaluate.js";
|
|
2
3
|
//#region src/calc/computeCalcFields.ts
|
|
3
4
|
/** Returns the calc expression if the field carries one (non-null object), otherwise undefined. */
|
|
@@ -14,7 +15,7 @@ const computeCalcFields = (fields, answers) => {
|
|
|
14
15
|
let next = answers;
|
|
15
16
|
for (const field of fields) {
|
|
16
17
|
const expr = calcExpressionOf(field);
|
|
17
|
-
if (expr) next = {
|
|
18
|
+
if (expr && isNamedField(field)) next = {
|
|
18
19
|
...next,
|
|
19
20
|
[field.name]: evaluateCalc(expr, next)
|
|
20
21
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"computeCalcFields.js","names":[],"sources":["../../src/calc/computeCalcFields.ts"],"sourcesContent":["import type { FormFieldInstance } from '../submissions/types'\nimport { evaluateCalc } from './evaluate'\nimport type { CalcExpression } from './types'\n\n/** Returns the calc expression if the field carries one (non-null object), otherwise undefined. */\nexport const calcExpressionOf = (field: FormFieldInstance): CalcExpression | undefined => {\n\tconst expr = field.expression\n\treturn expr !== null && typeof expr === 'object' && !Array.isArray(expr)\n\t\t? (expr as CalcExpression)\n\t\t: undefined\n}\n\n/**\n * Returns answers with every calc field's value derived from its expression,\n * folded in declaration order so a calc may reference an earlier calc's result.\n * Identity when no field carries an expression.\n */\nexport const computeCalcFields = (\n\tfields: FormFieldInstance[],\n\tanswers: Record<string, unknown>\n): Record<string, unknown> => {\n\tlet next = answers\n\tfor (const field of fields) {\n\t\tconst expr = calcExpressionOf(field)\n\t\tif (expr) {\n\t\t\tnext = { ...next, [field.name]: evaluateCalc(expr, next) }\n\t\t}\n\t}\n\treturn next\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"computeCalcFields.js","names":[],"sources":["../../src/calc/computeCalcFields.ts"],"sourcesContent":["import { isNamedField } from '../fields/fieldKey'\nimport type { FormFieldInstance } from '../submissions/types'\nimport { evaluateCalc } from './evaluate'\nimport type { CalcExpression } from './types'\n\n/** Returns the calc expression if the field carries one (non-null object), otherwise undefined. */\nexport const calcExpressionOf = (field: FormFieldInstance): CalcExpression | undefined => {\n\tconst expr = field.expression\n\treturn expr !== null && typeof expr === 'object' && !Array.isArray(expr)\n\t\t? (expr as CalcExpression)\n\t\t: undefined\n}\n\n/**\n * Returns answers with every calc field's value derived from its expression,\n * folded in declaration order so a calc may reference an earlier calc's result.\n * Identity when no field carries an expression.\n */\nexport const computeCalcFields = (\n\tfields: FormFieldInstance[],\n\tanswers: Record<string, unknown>\n): Record<string, unknown> => {\n\tlet next = answers\n\tfor (const field of fields) {\n\t\tconst expr = calcExpressionOf(field)\n\t\tif (expr && isNamedField(field)) {\n\t\t\tnext = { ...next, [field.name]: evaluateCalc(expr, next) }\n\t\t}\n\t}\n\treturn next\n}\n"],"mappings":";;;;AAMA,MAAa,oBAAoB,UAAyD;CACzF,MAAM,OAAO,MAAM;CACnB,OAAO,SAAS,QAAQ,OAAO,SAAS,YAAY,CAAC,MAAM,QAAQ,IAAI,IACnE,OACD,KAAA;AACJ;;;;;;AAOA,MAAa,qBACZ,QACA,YAC6B;CAC7B,IAAI,OAAO;CACX,KAAK,MAAM,SAAS,QAAQ;EAC3B,MAAM,OAAO,iBAAiB,KAAK;EACnC,IAAI,QAAQ,aAAa,KAAK,GAC7B,OAAO;GAAE,GAAG;IAAO,MAAM,OAAO,aAAa,MAAM,IAAI;EAAE;CAE3D;CACA,OAAO;AACR"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { TranslationKey } from "../translations/keys.js";
|
|
2
|
+
|
|
1
3
|
//#region src/client/ByteSizeField.d.ts
|
|
2
4
|
/** Props: standard number field client props; label and description arrive resolved on `field`. */
|
|
3
5
|
type ByteSizeFieldProps = {
|
|
@@ -9,6 +11,12 @@ type ByteSizeFieldProps = {
|
|
|
9
11
|
};
|
|
10
12
|
};
|
|
11
13
|
label?: unknown;
|
|
14
|
+
/**
|
|
15
|
+
* Field description as a translation key, resolved client-side. Payload drops `admin.description`
|
|
16
|
+
* functions from client fields, so a translated description must travel as a key; a static
|
|
17
|
+
* `admin.description` string still renders when this is unset.
|
|
18
|
+
*/
|
|
19
|
+
descriptionKey?: TranslationKey;
|
|
12
20
|
};
|
|
13
21
|
/**
|
|
14
22
|
* The `Field` mounted on a byte-count number config field (the file field's `maxSize`): a plain
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { formatBytes } from "../uploads/formatBytes.js";
|
|
3
|
+
import { useTranslation as useTranslation$1 } from "../translations/useTranslation.js";
|
|
3
4
|
import { toStaticLabel } from "./toStaticLabel.js";
|
|
4
5
|
import { FieldDescription, FieldLabel, useField } from "@payloadcms/ui";
|
|
5
6
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -12,7 +13,8 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
12
13
|
const ByteSizeField = (props) => {
|
|
13
14
|
const { path, setValue, value } = useField({ path: props.path });
|
|
14
15
|
const label = toStaticLabel(props.field?.label ?? props.label);
|
|
15
|
-
const
|
|
16
|
+
const { t } = useTranslation$1();
|
|
17
|
+
const description = props.descriptionKey ? t(props.descriptionKey) : toStaticLabel(props.field?.admin?.description);
|
|
16
18
|
const handleChange = (event) => {
|
|
17
19
|
if (event.target.value === "") {
|
|
18
20
|
setValue(null);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ByteSizeField.js","names":[],"sources":["../../src/client/ByteSizeField.tsx"],"sourcesContent":["'use client'\n\nimport { FieldDescription, FieldLabel, useField } from '@payloadcms/ui'\nimport type { ChangeEvent } from 'react'\nimport { formatBytes } from '../uploads/formatBytes'\nimport { toStaticLabel } from './toStaticLabel'\n\n/** Props: standard number field client props; label and description arrive resolved on `field`. */\nexport type ByteSizeFieldProps = {\n\tpath?: string\n\tfield?: { label?: unknown; admin?: { description?: unknown } }\n\tlabel?: unknown\n}\n\n/**\n * The `Field` mounted on a byte-count number config field (the file field's `maxSize`): a plain\n * number input that live-previews the value as a human-readable size ('= 2.5 MB') so authors are\n * not left decoding raw bytes. Stores the raw byte number unchanged.\n */\nexport const ByteSizeField = (props: ByteSizeFieldProps) => {\n\tconst { path, setValue, value } = useField<number>({ path: props.path })\n\tconst label = toStaticLabel(props.field?.label ?? props.label)\n\tconst description = toStaticLabel(props.field?.admin?.description)\n\n\tconst handleChange = (event: ChangeEvent<HTMLInputElement>) => {\n\t\tif (event.target.value === '') {\n\t\t\tsetValue(null)\n\t\t\treturn\n\t\t}\n\t\tconst parsed = Number(event.target.value)\n\t\tif (Number.isFinite(parsed)) {\n\t\t\tsetValue(parsed)\n\t\t}\n\t}\n\n\treturn (\n\t\t<div className=\"field-type number\" style={{ marginBlockEnd: '1rem' }}>\n\t\t\t<FieldLabel label={label} path={path} />\n\t\t\t<div className=\"field-type__wrap\">\n\t\t\t\t<input\n\t\t\t\t\tid={`field-${path.replace(/\\./g, '__')}`}\n\t\t\t\t\tmin={0}\n\t\t\t\t\tname={path}\n\t\t\t\t\tonChange={handleChange}\n\t\t\t\t\ttype=\"number\"\n\t\t\t\t\tvalue={typeof value === 'number' ? value : ''}\n\t\t\t\t/>\n\t\t\t\t{typeof value === 'number' && value > 0 ? (\n\t\t\t\t\t<div className=\"field-description\">{`= ${formatBytes(value)}`}</div>\n\t\t\t\t) : null}\n\t\t\t\t<FieldDescription description={description} path={path} />\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"ByteSizeField.js","names":["useTranslation"],"sources":["../../src/client/ByteSizeField.tsx"],"sourcesContent":["'use client'\n\nimport { FieldDescription, FieldLabel, useField } from '@payloadcms/ui'\nimport type { ChangeEvent } from 'react'\nimport type { TranslationKey } from '../translations/keys'\nimport { useTranslation } from '../translations/useTranslation'\nimport { formatBytes } from '../uploads/formatBytes'\nimport { toStaticLabel } from './toStaticLabel'\n\n/** Props: standard number field client props; label and description arrive resolved on `field`. */\nexport type ByteSizeFieldProps = {\n\tpath?: string\n\tfield?: { label?: unknown; admin?: { description?: unknown } }\n\tlabel?: unknown\n\t/**\n\t * Field description as a translation key, resolved client-side. Payload drops `admin.description`\n\t * functions from client fields, so a translated description must travel as a key; a static\n\t * `admin.description` string still renders when this is unset.\n\t */\n\tdescriptionKey?: TranslationKey\n}\n\n/**\n * The `Field` mounted on a byte-count number config field (the file field's `maxSize`): a plain\n * number input that live-previews the value as a human-readable size ('= 2.5 MB') so authors are\n * not left decoding raw bytes. Stores the raw byte number unchanged.\n */\nexport const ByteSizeField = (props: ByteSizeFieldProps) => {\n\tconst { path, setValue, value } = useField<number>({ path: props.path })\n\tconst label = toStaticLabel(props.field?.label ?? props.label)\n\tconst { t } = useTranslation()\n\tconst description = props.descriptionKey\n\t\t? t(props.descriptionKey)\n\t\t: toStaticLabel(props.field?.admin?.description)\n\n\tconst handleChange = (event: ChangeEvent<HTMLInputElement>) => {\n\t\tif (event.target.value === '') {\n\t\t\tsetValue(null)\n\t\t\treturn\n\t\t}\n\t\tconst parsed = Number(event.target.value)\n\t\tif (Number.isFinite(parsed)) {\n\t\t\tsetValue(parsed)\n\t\t}\n\t}\n\n\treturn (\n\t\t<div className=\"field-type number\" style={{ marginBlockEnd: '1rem' }}>\n\t\t\t<FieldLabel label={label} path={path} />\n\t\t\t<div className=\"field-type__wrap\">\n\t\t\t\t<input\n\t\t\t\t\tid={`field-${path.replace(/\\./g, '__')}`}\n\t\t\t\t\tmin={0}\n\t\t\t\t\tname={path}\n\t\t\t\t\tonChange={handleChange}\n\t\t\t\t\ttype=\"number\"\n\t\t\t\t\tvalue={typeof value === 'number' ? value : ''}\n\t\t\t\t/>\n\t\t\t\t{typeof value === 'number' && value > 0 ? (\n\t\t\t\t\t<div className=\"field-description\">{`= ${formatBytes(value)}`}</div>\n\t\t\t\t) : null}\n\t\t\t\t<FieldDescription description={description} path={path} />\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n"],"mappings":";;;;;;;;;;;;AA2BA,MAAa,iBAAiB,UAA8B;CAC3D,MAAM,EAAE,MAAM,UAAU,UAAU,SAAiB,EAAE,MAAM,MAAM,KAAK,CAAC;CACvE,MAAM,QAAQ,cAAc,MAAM,OAAO,SAAS,MAAM,KAAK;CAC7D,MAAM,EAAE,MAAMA,iBAAe;CAC7B,MAAM,cAAc,MAAM,iBACvB,EAAE,MAAM,cAAc,IACtB,cAAc,MAAM,OAAO,OAAO,WAAW;CAEhD,MAAM,gBAAgB,UAAyC;EAC9D,IAAI,MAAM,OAAO,UAAU,IAAI;GAC9B,SAAS,IAAI;GACb;EACD;EACA,MAAM,SAAS,OAAO,MAAM,OAAO,KAAK;EACxC,IAAI,OAAO,SAAS,MAAM,GACzB,SAAS,MAAM;CAEjB;CAEA,OACC,qBAAC,OAAD;EAAK,WAAU;EAAoB,OAAO,EAAE,gBAAgB,OAAO;YAAnE,CACC,oBAAC,YAAD;GAAmB;GAAa;EAAO,CAAA,GACvC,qBAAC,OAAD;GAAK,WAAU;aAAf;IACC,oBAAC,SAAD;KACC,IAAI,SAAS,KAAK,QAAQ,OAAO,IAAI;KACrC,KAAK;KACL,MAAM;KACN,UAAU;KACV,MAAK;KACL,OAAO,OAAO,UAAU,WAAW,QAAQ;IAC3C,CAAA;IACA,OAAO,UAAU,YAAY,QAAQ,IACrC,oBAAC,OAAD;KAAK,WAAU;eAAqB,KAAK,YAAY,KAAK;IAAS,CAAA,IAChE;IACJ,oBAAC,kBAAD;KAA+B;KAAmB;IAAO,CAAA;GACrD;IACD;;AAEP"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/client/ClosePollButton.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The `poll.closePoll` UI field: one button that closes the poll and, once closed, reopens it, by
|
|
4
|
+
* saving the whole document rather than calling the `/:id/close` endpoint. Closing stamps
|
|
5
|
+
* `poll.closesAt` now while keeping the admin's just-picked winner, so both persist together through
|
|
6
|
+
* the normal save (`pollOutcomeBeforeChange` validates the winner and stamps `resolvedAt`; the
|
|
7
|
+
* `afterChange` hook enqueues the close for a `mostVoted`/`source` strategy). Reopening clears both
|
|
8
|
+
* `closesAt` and `winningValues`, since either one keeps a poll finalized. A `manual` poll with no
|
|
9
|
+
* winner selected disables the button (there is nothing to record); `mostVoted`/`source` stay enabled
|
|
10
|
+
* because the winner is server-computed. Disabled on an unsaved document (no id yet).
|
|
11
|
+
*/
|
|
12
|
+
declare const ClosePollButton: () => import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { ClosePollButton };
|
|
15
|
+
//# sourceMappingURL=ClosePollButton.d.ts.map
|