@10x-media/form-builder 0.1.0-beta.3 → 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 +125 -0
- package/README.md +4 -4
- package/dist/actions/body/serializeBody.d.ts +22 -4
- package/dist/actions/body/serializeBody.js +3 -1
- 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 +51 -12
- package/dist/actions/builtin/confirmation.js.map +1 -1
- package/dist/actions/builtin/emailTeam.js +82 -34
- package/dist/actions/builtin/emailTeam.js.map +1 -1
- package/dist/actions/builtin/index.d.ts +18 -1
- package/dist/actions/builtin/index.js +21 -7
- package/dist/actions/builtin/index.js.map +1 -1
- package/dist/actions/builtin/signedWebhook.js +2 -16
- package/dist/actions/builtin/signedWebhook.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/runActions.js +2 -0
- package/dist/actions/runActions.js.map +1 -1
- 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 +35 -6
- package/dist/aggregation/resolveResultsRequest.js +68 -11
- 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 +134 -83
- 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 +83 -5
- package/dist/client/endpointOptions.js +29 -0
- package/dist/client/endpointOptions.js.map +1 -0
- package/dist/client/flowAuthoring.js +142 -0
- package/dist/client/flowAuthoring.js.map +1 -0
- package/dist/client/synthesizeClientField.js +21 -10
- 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 +55 -12
- package/dist/collections/formSubmissions.js.map +1 -1
- package/dist/collections/forms.js +376 -57
- package/dist/collections/forms.js.map +1 -1
- package/dist/collections/redirectFields.d.ts +29 -0
- package/dist/conditions/conditionType.js +9 -2
- package/dist/conditions/conditionType.js.map +1 -1
- package/dist/conditions/normalizeConditions.js +2 -1
- package/dist/conditions/normalizeConditions.js.map +1 -1
- 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 +15 -6
- package/dist/exports/react.js +11 -3
- package/dist/exports/rsc.d.ts +8 -1
- package/dist/exports/rsc.js +8 -1
- package/dist/exports/types.d.ts +18 -10
- package/dist/fields/buildFieldBlocks.js +43 -19
- 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 +25 -15
- 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 +13 -8
- package/dist/fields/builtin/file.js.map +1 -1
- package/dist/fields/builtin/index.d.ts +13 -1
- package/dist/fields/builtin/index.js +20 -7
- package/dist/fields/builtin/index.js.map +1 -1
- package/dist/fields/builtin/message.js +30 -0
- package/dist/fields/builtin/message.js.map +1 -0
- package/dist/fields/builtin/repeater.js +8 -3
- package/dist/fields/builtin/repeater.js.map +1 -1
- package/dist/fields/builtin/select.js +8 -3
- 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/localizedIf.d.ts +13 -0
- package/dist/fields/localizedIf.js +12 -0
- package/dist/fields/localizedIf.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 +101 -51
- package/dist/fields/sharedConfig.js.map +1 -1
- package/dist/fields/types.d.ts +61 -5
- 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 +22 -7
- package/dist/flow/normalizeFlow.js.map +1 -1
- package/dist/flow/stepLabel.js +15 -0
- package/dist/flow/stepLabel.js.map +1 -0
- 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 +22 -0
- package/dist/form/pollState.js +17 -0
- package/dist/form/pollState.js.map +1 -0
- package/dist/form/toFormDocument.d.ts +85 -0
- package/dist/form/toFormDocument.js +75 -0
- package/dist/form/toFormDocument.js.map +1 -0
- package/dist/form/types.d.ts +65 -0
- package/dist/index.d.ts +193 -27
- package/dist/index.js +59 -19
- 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 +33 -44
- package/dist/plugin/registerCollections.js.map +1 -1
- package/dist/plugin/uploadsCollection.d.ts +13 -0
- package/dist/plugin/uploadsCollection.js +44 -0
- package/dist/plugin/uploadsCollection.js.map +1 -0
- package/dist/poll/applyPollOptions.js +18 -0
- package/dist/poll/applyPollOptions.js.map +1 -0
- package/dist/poll/buildPollOptionSourceFields.js +50 -0
- package/dist/poll/buildPollOptionSourceFields.js.map +1 -0
- 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 +44 -0
- package/dist/poll/definePollOptionSource.js +6 -0
- package/dist/poll/definePollOptionSource.js.map +1 -0
- 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/registry.d.ts +16 -0
- package/dist/poll/registry.js +16 -0
- package/dist/poll/registry.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 +29 -0
- package/dist/poll/resolvePollOptions.js +52 -0
- package/dist/poll/resolvePollOptions.js.map +1 -0
- package/dist/poll/resolvePollOptionsRequest.js +51 -0
- package/dist/poll/resolvePollOptionsRequest.js.map +1 -0
- package/dist/poll/resolvePollOutcome.d.ts +34 -0
- package/dist/poll/resolvePollOutcome.js +76 -0
- package/dist/poll/resolvePollOutcome.js.map +1 -0
- package/dist/poll/resultsField.js +21 -0
- package/dist/poll/resultsField.js.map +1 -0
- package/dist/prefill/valuesFromSearchParams.js +3 -2
- package/dist/prefill/valuesFromSearchParams.js.map +1 -1
- package/dist/presentations/types.d.ts +3 -2
- package/dist/react/Form.d.ts +15 -33
- package/dist/react/Form.js +144 -119
- package/dist/react/Form.js.map +1 -1
- package/dist/react/FormContext.d.ts +37 -1
- package/dist/react/FormContext.js.map +1 -1
- package/dist/react/FormControls.d.ts +55 -0
- package/dist/react/FormControls.js +56 -0
- package/dist/react/FormControls.js.map +1 -0
- package/dist/react/FormResults.d.ts +6 -3
- package/dist/react/FormResults.js +37 -29
- package/dist/react/FormResults.js.map +1 -1
- package/dist/react/FormSteps.d.ts +17 -0
- package/dist/react/FormSteps.js +35 -0
- package/dist/react/FormSteps.js.map +1 -0
- package/dist/react/Poll.d.ts +17 -6
- package/dist/react/Poll.js +66 -18
- 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/fetchResults.d.ts +1 -1
- package/dist/react/fetchResults.js +1 -1
- package/dist/react/fetchResults.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 +6 -3
- package/dist/react/renderers/file.js.map +1 -1
- package/dist/react/renderers/index.js +5 -0
- package/dist/react/renderers/index.js.map +1 -1
- package/dist/react/renderers/message.js +35 -0
- package/dist/react/renderers/message.js.map +1 -0
- 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 +23 -2
- package/dist/react/state.js +16 -7
- package/dist/react/state.js.map +1 -1
- package/dist/react/uploadFile.d.ts +2 -2
- package/dist/react/uploadFile.js +1 -1
- package/dist/react/uploadFile.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 +92 -44
- package/dist/submissions/runSubmission.js.map +1 -1
- package/dist/submissions/types.d.ts +11 -4
- package/dist/submissions/validateSubmission.js +59 -10
- package/dist/submissions/validateSubmission.js.map +1 -1
- package/dist/submissions/votedCookie.d.ts +13 -0
- package/dist/submissions/votedCookie.js +27 -0
- package/dist/submissions/votedCookie.js.map +1 -0
- package/dist/translations/de.js +295 -0
- package/dist/translations/de.js.map +1 -0
- package/dist/translations/en.js +138 -23
- package/dist/translations/en.js.map +1 -1
- package/dist/translations/index.d.ts +2 -1
- package/dist/translations/index.js +6 -2
- package/dist/translations/index.js.map +1 -1
- package/dist/translations/keys.d.ts +134 -19
- package/dist/translations/keys.js +135 -20
- package/dist/translations/keys.js.map +1 -1
- package/dist/uploads/types.d.ts +1 -2
- 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/dist/validation/validateUrl.js +22 -0
- package/dist/validation/validateUrl.js.map +1 -0
- package/package.json +7 -6
- package/styles/form-builder.css +10 -0
- package/dist/collections/uploads.d.ts +0 -30
- package/dist/collections/uploads.js +0 -55
- package/dist/collections/uploads.js.map +0 -1
- package/dist/consent/buildConsentSourceConfig.js +0 -49
- package/dist/consent/buildConsentSourceConfig.js.map +0 -1
- package/dist/consent/builtin/index.d.ts +0 -7
- package/dist/consent/builtin/index.js +0 -11
- 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 -44
- 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
- package/dist/presentations/registry.d.ts +0 -15
- package/dist/presentations/registry.js +0 -15
- package/dist/presentations/registry.js.map +0 -1
- package/dist/react/toFormDocument.d.ts +0 -24
- package/dist/react/toFormDocument.js +0 -22
- package/dist/react/toFormDocument.js.map +0 -1
|
@@ -1,44 +1,92 @@
|
|
|
1
|
+
import { localizedIf } from "../../fields/localizedIf.js";
|
|
1
2
|
import { interpolate } from "../../recall/interpolate.js";
|
|
2
3
|
import { keys } from "../../translations/keys.js";
|
|
3
4
|
import { labelFor } from "../../translations/server.js";
|
|
4
5
|
import { resolverFor } from "../body/serializeBody.js";
|
|
5
6
|
import { defineAction } from "../defineAction.js";
|
|
7
|
+
import { buildFromField } from "../fromAddresses.js";
|
|
8
|
+
import { buildToField } from "../../email/departments.js";
|
|
6
9
|
//#region src/actions/builtin/emailTeam.ts
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
10
|
+
/**
|
|
11
|
+
* `subject` and `body` are email content and follow `localize`; `from` is an address that never
|
|
12
|
+
* does. `to` is localized (following `localize`): a submission's locale selects the stored `to` at
|
|
13
|
+
* send, so a German submission routes to the German address. `cc`, `bcc`, and `replyTo` follow the
|
|
14
|
+
* same localized-address posture as `to`, each interpolated the same as `subject` so `{{field}}`
|
|
15
|
+
* merge tags work; a comma-separated list is forwarded as-is, since `payload.sendEmail`
|
|
16
|
+
* (nodemailer) accepts one directly. `editor` overrides the body field's Lexical/richText editor
|
|
17
|
+
* (from the plugin's `richText.editor` option). `fromAddresses`, when given (the plugin's
|
|
18
|
+
* `email.fromAddresses` option), adds a `from` select sourced from the host resolver; absent, no
|
|
19
|
+
* `from` field exists and every send uses the email adapter's default sender. `departments`, when
|
|
20
|
+
* given (the plugin's `email.departments` option), turns `to` into a select whose options are the
|
|
21
|
+
* host's resolved departments; absent, `to` stays a plain (localized) text field.
|
|
22
|
+
*/
|
|
23
|
+
const buildEmailTeam = (localize, editor, fromAddresses, departments) => {
|
|
24
|
+
const toField = departments ? buildToField(departments, localize) : {
|
|
25
|
+
name: "to",
|
|
26
|
+
type: "text",
|
|
27
|
+
label: labelFor(keys.actionConfigTo),
|
|
28
|
+
...localizedIf(localize)
|
|
29
|
+
};
|
|
30
|
+
return defineAction({
|
|
31
|
+
type: "emailTeam",
|
|
32
|
+
label: keys.actionEmailTeam,
|
|
33
|
+
config: [
|
|
34
|
+
toField,
|
|
35
|
+
...fromAddresses ? [buildFromField(fromAddresses)] : [],
|
|
36
|
+
{
|
|
37
|
+
name: "cc",
|
|
38
|
+
type: "text",
|
|
39
|
+
label: labelFor(keys.actionConfigCc),
|
|
40
|
+
...localizedIf(localize)
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "bcc",
|
|
44
|
+
type: "text",
|
|
45
|
+
label: labelFor(keys.actionConfigBcc),
|
|
46
|
+
...localizedIf(localize)
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "replyTo",
|
|
50
|
+
type: "text",
|
|
51
|
+
label: labelFor(keys.actionConfigReplyTo),
|
|
52
|
+
...localizedIf(localize)
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "subject",
|
|
56
|
+
type: "text",
|
|
57
|
+
label: labelFor(keys.actionConfigSubject),
|
|
58
|
+
...localizedIf(localize)
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "body",
|
|
62
|
+
type: "richText",
|
|
63
|
+
label: labelFor(keys.actionConfigBody),
|
|
64
|
+
admin: { description: labelFor(keys.actionConfigBodyDescription) },
|
|
65
|
+
...localizedIf(localize),
|
|
66
|
+
...editor ? { editor } : {}
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
run: async (args) => {
|
|
70
|
+
const { config, values, payload, renderBody } = args;
|
|
71
|
+
if (!config.to) throw new Error("emailTeam: missing \"to\" address");
|
|
72
|
+
if (typeof payload.sendEmail !== "function") throw new Error("emailTeam: no email adapter configured");
|
|
73
|
+
const resolve = resolverFor(values);
|
|
74
|
+
const subject = interpolate(config.subject ?? "", resolve);
|
|
75
|
+
const html = await renderBody(config.body);
|
|
76
|
+
await payload.sendEmail({
|
|
77
|
+
to: config.to,
|
|
78
|
+
subject,
|
|
79
|
+
html,
|
|
80
|
+
...config.from ? { from: config.from } : {},
|
|
81
|
+
...config.cc ? { cc: interpolate(config.cc, resolve) } : {},
|
|
82
|
+
...config.bcc ? { bcc: interpolate(config.bcc, resolve) } : {},
|
|
83
|
+
...config.replyTo ? { replyTo: interpolate(config.replyTo, resolve) } : {}
|
|
84
|
+
});
|
|
26
85
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (!config.to) throw new Error("emailTeam: missing \"to\" address");
|
|
31
|
-
if (typeof payload.sendEmail !== "function") throw new Error("emailTeam: no email adapter configured");
|
|
32
|
-
const subject = interpolate(config.subject ?? "", resolverFor(values));
|
|
33
|
-
const html = await renderBody(config.body);
|
|
34
|
-
await payload.sendEmail({
|
|
35
|
-
to: config.to,
|
|
36
|
-
subject,
|
|
37
|
-
html
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
});
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
buildEmailTeam(true);
|
|
41
89
|
//#endregion
|
|
42
|
-
export {
|
|
90
|
+
export { buildEmailTeam };
|
|
43
91
|
|
|
44
92
|
//# sourceMappingURL=emailTeam.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"emailTeam.js","names":[],"sources":["../../../src/actions/builtin/emailTeam.ts"],"sourcesContent":["import { interpolate } from '../../recall/interpolate'\nimport { keys } from '../../translations/keys'\nimport { labelFor } from '../../translations/server'\nimport { resolverFor } from '../body/serializeBody'\nimport { defineAction } from '../defineAction'\n\ntype EmailTeamConfig = {
|
|
1
|
+
{"version":3,"file":"emailTeam.js","names":[],"sources":["../../../src/actions/builtin/emailTeam.ts"],"sourcesContent":["import type { RichTextField, TextField } from 'payload'\nimport type { DepartmentEmailsResolver } from '../../email/departments'\nimport { buildToField } from '../../email/departments'\nimport { localizedIf } from '../../fields/localizedIf'\nimport { interpolate } from '../../recall/interpolate'\nimport { keys } from '../../translations/keys'\nimport { labelFor } from '../../translations/server'\nimport { resolverFor } from '../body/serializeBody'\nimport { defineAction } from '../defineAction'\nimport { buildFromField, type FromAddressesResolver } from '../fromAddresses'\n\ntype EmailTeamConfig = {\n\tto?: string\n\tfrom?: string\n\tcc?: string\n\tbcc?: string\n\treplyTo?: string\n\tsubject?: string\n\tbody?: unknown\n}\n\n/**\n * `subject` and `body` are email content and follow `localize`; `from` is an address that never\n * does. `to` is localized (following `localize`): a submission's locale selects the stored `to` at\n * send, so a German submission routes to the German address. `cc`, `bcc`, and `replyTo` follow the\n * same localized-address posture as `to`, each interpolated the same as `subject` so `{{field}}`\n * merge tags work; a comma-separated list is forwarded as-is, since `payload.sendEmail`\n * (nodemailer) accepts one directly. `editor` overrides the body field's Lexical/richText editor\n * (from the plugin's `richText.editor` option). `fromAddresses`, when given (the plugin's\n * `email.fromAddresses` option), adds a `from` select sourced from the host resolver; absent, no\n * `from` field exists and every send uses the email adapter's default sender. `departments`, when\n * given (the plugin's `email.departments` option), turns `to` into a select whose options are the\n * host's resolved departments; absent, `to` stays a plain (localized) text field.\n */\n// biome-ignore lint/complexity/useMaxParams: positional args mirror the fromAddresses threading (localize, editor, fromAddresses, departments)\nexport const buildEmailTeam = (\n\tlocalize: boolean,\n\teditor?: RichTextField['editor'],\n\tfromAddresses?: FromAddressesResolver,\n\tdepartments?: DepartmentEmailsResolver\n) => {\n\tconst toField: TextField = departments\n\t\t? buildToField(departments, localize)\n\t\t: { name: 'to', type: 'text', label: labelFor(keys.actionConfigTo), ...localizedIf(localize) }\n\treturn defineAction<EmailTeamConfig>({\n\t\ttype: 'emailTeam',\n\t\tlabel: keys.actionEmailTeam,\n\t\tconfig: [\n\t\t\ttoField,\n\t\t\t...(fromAddresses ? [buildFromField(fromAddresses)] : []),\n\t\t\t{\n\t\t\t\tname: 'cc',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelFor(keys.actionConfigCc),\n\t\t\t\t...localizedIf(localize),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'bcc',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelFor(keys.actionConfigBcc),\n\t\t\t\t...localizedIf(localize),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'replyTo',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelFor(keys.actionConfigReplyTo),\n\t\t\t\t...localizedIf(localize),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'subject',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelFor(keys.actionConfigSubject),\n\t\t\t\t...localizedIf(localize),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'body',\n\t\t\t\ttype: 'richText',\n\t\t\t\tlabel: labelFor(keys.actionConfigBody),\n\t\t\t\tadmin: { description: labelFor(keys.actionConfigBodyDescription) },\n\t\t\t\t...localizedIf(localize),\n\t\t\t\t...(editor ? { editor } : {}),\n\t\t\t},\n\t\t],\n\t\trun: async (args) => {\n\t\t\tconst { config, values, payload, renderBody } = args\n\n\t\t\tif (!config.to) {\n\t\t\t\tthrow new Error('emailTeam: missing \"to\" address')\n\t\t\t}\n\n\t\t\tif (typeof payload.sendEmail !== 'function') {\n\t\t\t\tthrow new Error('emailTeam: no email adapter configured')\n\t\t\t}\n\n\t\t\tconst resolve = resolverFor(values)\n\t\t\tconst subject = interpolate(config.subject ?? '', resolve)\n\t\t\tconst html = await renderBody(config.body)\n\n\t\t\t// `from` was validated at save time against `fromAddresses(req)`; not re-checked here\n\t\t\t// (the job's `req` may differ from the authoring admin's, and the config is\n\t\t\t// admin-authored, not visitor-controlled), so the stored value is forwarded verbatim.\n\t\t\tawait payload.sendEmail({\n\t\t\t\tto: config.to,\n\t\t\t\tsubject,\n\t\t\t\thtml,\n\t\t\t\t...(config.from ? { from: config.from } : {}),\n\t\t\t\t...(config.cc ? { cc: interpolate(config.cc, resolve) } : {}),\n\t\t\t\t...(config.bcc ? { bcc: interpolate(config.bcc, resolve) } : {}),\n\t\t\t\t...(config.replyTo ? { replyTo: interpolate(config.replyTo, resolve) } : {}),\n\t\t\t})\n\t\t},\n\t})\n}\n\nexport const emailTeam = buildEmailTeam(true)\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAmCA,MAAa,kBACZ,UACA,QACA,eACA,gBACI;CACJ,MAAM,UAAqB,cACxB,aAAa,aAAa,QAAQ,IAClC;EAAE,MAAM;EAAM,MAAM;EAAQ,OAAO,SAAS,KAAK,cAAc;EAAG,GAAG,YAAY,QAAQ;CAAE;CAC9F,OAAO,aAA8B;EACpC,MAAM;EACN,OAAO,KAAK;EACZ,QAAQ;GACP;GACA,GAAI,gBAAgB,CAAC,eAAe,aAAa,CAAC,IAAI,CAAC;GACvD;IACC,MAAM;IACN,MAAM;IACN,OAAO,SAAS,KAAK,cAAc;IACnC,GAAG,YAAY,QAAQ;GACxB;GACA;IACC,MAAM;IACN,MAAM;IACN,OAAO,SAAS,KAAK,eAAe;IACpC,GAAG,YAAY,QAAQ;GACxB;GACA;IACC,MAAM;IACN,MAAM;IACN,OAAO,SAAS,KAAK,mBAAmB;IACxC,GAAG,YAAY,QAAQ;GACxB;GACA;IACC,MAAM;IACN,MAAM;IACN,OAAO,SAAS,KAAK,mBAAmB;IACxC,GAAG,YAAY,QAAQ;GACxB;GACA;IACC,MAAM;IACN,MAAM;IACN,OAAO,SAAS,KAAK,gBAAgB;IACrC,OAAO,EAAE,aAAa,SAAS,KAAK,2BAA2B,EAAE;IACjE,GAAG,YAAY,QAAQ;IACvB,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;GAC5B;EACD;EACA,KAAK,OAAO,SAAS;GACpB,MAAM,EAAE,QAAQ,QAAQ,SAAS,eAAe;GAEhD,IAAI,CAAC,OAAO,IACX,MAAM,IAAI,MAAM,mCAAiC;GAGlD,IAAI,OAAO,QAAQ,cAAc,YAChC,MAAM,IAAI,MAAM,wCAAwC;GAGzD,MAAM,UAAU,YAAY,MAAM;GAClC,MAAM,UAAU,YAAY,OAAO,WAAW,IAAI,OAAO;GACzD,MAAM,OAAO,MAAM,WAAW,OAAO,IAAI;GAKzC,MAAM,QAAQ,UAAU;IACvB,IAAI,OAAO;IACX;IACA;IACA,GAAI,OAAO,OAAO,EAAE,MAAM,OAAO,KAAK,IAAI,CAAC;IAC3C,GAAI,OAAO,KAAK,EAAE,IAAI,YAAY,OAAO,IAAI,OAAO,EAAE,IAAI,CAAC;IAC3D,GAAI,OAAO,MAAM,EAAE,KAAK,YAAY,OAAO,KAAK,OAAO,EAAE,IAAI,CAAC;IAC9D,GAAI,OAAO,UAAU,EAAE,SAAS,YAAY,OAAO,SAAS,OAAO,EAAE,IAAI,CAAC;GAC3E,CAAC;EACF;CACD,CAAC;AACF;AAEyB,eAAe,IAAI"}
|
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
import { AnyActionDefinition } from "../defineAction.js";
|
|
2
|
+
import { FromAddressesResolver } from "../fromAddresses.js";
|
|
3
|
+
import { DepartmentEmailsResolver } from "../../email/departments.js";
|
|
4
|
+
import { RichTextField } from "payload";
|
|
2
5
|
|
|
3
6
|
//#region src/actions/builtin/index.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* Built-in action definitions with content-bearing config fields (email subjects and rich text
|
|
9
|
+
* bodies) carrying `localized: true` when `localize` is true. `editor`, when given, overrides
|
|
10
|
+
* the Lexical/richText editor on both action bodies (the plugin passes
|
|
11
|
+
* `richText.bodyEditor ?? richText.editor`). `fromAddresses`, when given (the plugin's
|
|
12
|
+
* `email.fromAddresses` option), adds a `from` select to both email actions. `departments`, when
|
|
13
|
+
* given (the plugin's `email.departments` option), turns the `emailTeam` `to` into a select over the
|
|
14
|
+
* host's resolved departments. `signedWebhook` has no content fields and is shared static.
|
|
15
|
+
*/
|
|
16
|
+
declare const buildDefaultActionDefinitions: (localize: boolean, editor?: RichTextField["editor"], fromAddresses?: FromAddressesResolver, departments?: DepartmentEmailsResolver) => Record<string, AnyActionDefinition>;
|
|
17
|
+
/**
|
|
18
|
+
* Prebuilt with content localization on; with `localizeContent: false`, derive overrides from
|
|
19
|
+
* `buildDefaultActionDefinitions(false)` instead so a spread does not reintroduce `localized`.
|
|
20
|
+
*/
|
|
4
21
|
declare const defaultActionDefinitions: Record<string, AnyActionDefinition>;
|
|
5
22
|
//#endregion
|
|
6
|
-
export { defaultActionDefinitions };
|
|
23
|
+
export { buildDefaultActionDefinitions, defaultActionDefinitions };
|
|
7
24
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,13 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { buildConfirmation } from "./confirmation.js";
|
|
2
|
+
import { buildEmailTeam } from "./emailTeam.js";
|
|
3
3
|
import { signedWebhook } from "./signedWebhook.js";
|
|
4
4
|
//#region src/actions/builtin/index.ts
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Built-in action definitions with content-bearing config fields (email subjects and rich text
|
|
7
|
+
* bodies) carrying `localized: true` when `localize` is true. `editor`, when given, overrides
|
|
8
|
+
* the Lexical/richText editor on both action bodies (the plugin passes
|
|
9
|
+
* `richText.bodyEditor ?? richText.editor`). `fromAddresses`, when given (the plugin's
|
|
10
|
+
* `email.fromAddresses` option), adds a `from` select to both email actions. `departments`, when
|
|
11
|
+
* given (the plugin's `email.departments` option), turns the `emailTeam` `to` into a select over the
|
|
12
|
+
* host's resolved departments. `signedWebhook` has no content fields and is shared static.
|
|
13
|
+
*/
|
|
14
|
+
const buildDefaultActionDefinitions = (localize, editor, fromAddresses, departments) => ({
|
|
15
|
+
emailTeam: buildEmailTeam(localize, editor, fromAddresses, departments),
|
|
16
|
+
confirmation: buildConfirmation(localize, editor, fromAddresses),
|
|
8
17
|
signedWebhook
|
|
9
|
-
};
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* Prebuilt with content localization on; with `localizeContent: false`, derive overrides from
|
|
21
|
+
* `buildDefaultActionDefinitions(false)` instead so a spread does not reintroduce `localized`.
|
|
22
|
+
*/
|
|
23
|
+
const defaultActionDefinitions = buildDefaultActionDefinitions(true);
|
|
10
24
|
//#endregion
|
|
11
|
-
export { defaultActionDefinitions };
|
|
25
|
+
export { buildDefaultActionDefinitions, defaultActionDefinitions };
|
|
12
26
|
|
|
13
27
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/actions/builtin/index.ts"],"sourcesContent":["import type { AnyActionDefinition } from '../defineAction'\nimport {
|
|
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,23 +1,9 @@
|
|
|
1
1
|
import { keys } from "../../translations/keys.js";
|
|
2
|
-
import {
|
|
2
|
+
import { labelFor } from "../../translations/server.js";
|
|
3
3
|
import { defineAction } from "../defineAction.js";
|
|
4
|
+
import { validateUrl } from "../../validation/validateUrl.js";
|
|
4
5
|
import { SIGNATURE_HEADER, signPayload } from "../sign.js";
|
|
5
6
|
//#region src/actions/builtin/signedWebhook.ts
|
|
6
|
-
/**
|
|
7
|
-
* Validates the signed-webhook action's `url`: unset is fine (the runtime throws on a missing
|
|
8
|
-
* URL when the action actually runs), otherwise it must parse as an absolute http(s) URL.
|
|
9
|
-
* Exported for unit testing.
|
|
10
|
-
*/
|
|
11
|
-
const validateUrl = (value, { req }) => {
|
|
12
|
-
if (typeof value !== "string" || value.length === 0) return true;
|
|
13
|
-
let parsed;
|
|
14
|
-
try {
|
|
15
|
-
parsed = new URL(value);
|
|
16
|
-
} catch {
|
|
17
|
-
return asTranslate(req.t)(keys.validationUrlInvalid);
|
|
18
|
-
}
|
|
19
|
-
return parsed.protocol === "http:" || parsed.protocol === "https:" ? true : asTranslate(req.t)(keys.validationUrlInvalid);
|
|
20
|
-
};
|
|
21
7
|
const signedWebhook = defineAction({
|
|
22
8
|
type: "signedWebhook",
|
|
23
9
|
label: keys.actionSignedWebhook,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signedWebhook.js","names":[],"sources":["../../../src/actions/builtin/signedWebhook.ts"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"file":"signedWebhook.js","names":[],"sources":["../../../src/actions/builtin/signedWebhook.ts"],"sourcesContent":["import { keys } from '../../translations/keys'\nimport { labelFor } from '../../translations/server'\nimport { validateUrl } from '../../validation/validateUrl'\nimport { defineAction } from '../defineAction'\nimport { SIGNATURE_HEADER, signPayload } from '../sign'\n\nexport { validateUrl } from '../../validation/validateUrl'\n\ntype SignedWebhookConfig = { url?: string; secret?: string }\n\nexport const signedWebhook = defineAction<SignedWebhookConfig>({\n\ttype: 'signedWebhook',\n\tlabel: keys.actionSignedWebhook,\n\tconfig: [\n\t\t{\n\t\t\tname: 'url',\n\t\t\ttype: 'text',\n\t\t\tlabel: labelFor(keys.actionConfigUrl),\n\t\t\tadmin: { description: labelFor(keys.actionConfigUrlDescription) },\n\t\t\tvalidate: validateUrl,\n\t\t},\n\t\t{\n\t\t\tname: 'secret',\n\t\t\ttype: 'text',\n\t\t\tlabel: labelFor(keys.actionConfigSecret),\n\t\t\tadmin: { description: labelFor(keys.actionConfigSecretDescription) },\n\t\t},\n\t],\n\trun: async (args) => {\n\t\tconst { config, form, submissionId, values } = args\n\n\t\tif (!config.url) {\n\t\t\tthrow new Error('signedWebhook: missing \"url\"')\n\t\t}\n\t\tif (!config.secret) {\n\t\t\tthrow new Error('signedWebhook: missing \"secret\"')\n\t\t}\n\n\t\tconst body = JSON.stringify({ formId: form.id, submissionId, values })\n\t\tconst controller = new AbortController()\n\t\tconst timer = setTimeout(() => controller.abort(), 10_000)\n\n\t\tlet response: Response\n\t\ttry {\n\t\t\tresponse = await fetch(config.url, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t[SIGNATURE_HEADER]: signPayload(body, config.secret),\n\t\t\t\t},\n\t\t\t\tbody,\n\t\t\t\tsignal: controller.signal,\n\t\t\t})\n\t\t} finally {\n\t\t\tclearTimeout(timer)\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(`signedWebhook: server responded ${response.status}`)\n\t\t}\n\t},\n})\n"],"mappings":";;;;;;AAUA,MAAa,gBAAgB,aAAkC;CAC9D,MAAM;CACN,OAAO,KAAK;CACZ,QAAQ,CACP;EACC,MAAM;EACN,MAAM;EACN,OAAO,SAAS,KAAK,eAAe;EACpC,OAAO,EAAE,aAAa,SAAS,KAAK,0BAA0B,EAAE;EAChE,UAAU;CACX,GACA;EACC,MAAM;EACN,MAAM;EACN,OAAO,SAAS,KAAK,kBAAkB;EACvC,OAAO,EAAE,aAAa,SAAS,KAAK,6BAA6B,EAAE;CACpE,CACD;CACA,KAAK,OAAO,SAAS;EACpB,MAAM,EAAE,QAAQ,MAAM,cAAc,WAAW;EAE/C,IAAI,CAAC,OAAO,KACX,MAAM,IAAI,MAAM,gCAA8B;EAE/C,IAAI,CAAC,OAAO,QACX,MAAM,IAAI,MAAM,mCAAiC;EAGlD,MAAM,OAAO,KAAK,UAAU;GAAE,QAAQ,KAAK;GAAI;GAAc;EAAO,CAAC;EACrE,MAAM,aAAa,IAAI,gBAAgB;EACvC,MAAM,QAAQ,iBAAiB,WAAW,MAAM,GAAG,GAAM;EAEzD,IAAI;EACJ,IAAI;GACH,WAAW,MAAM,MAAM,OAAO,KAAK;IAClC,QAAQ;IACR,SAAS;KACR,gBAAgB;MACf,mBAAmB,YAAY,MAAM,OAAO,MAAM;IACpD;IACA;IACA,QAAQ,WAAW;GACpB,CAAC;EACF,UAAU;GACT,aAAa,KAAK;EACnB;EAEA,IAAI,CAAC,SAAS,IACb,MAAM,IAAI,MAAM,mCAAmC,SAAS,QAAQ;CAEtE;AACD,CAAC"}
|
|
@@ -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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runActions.js","names":[],"sources":["../../src/actions/runActions.ts"],"sourcesContent":["import type { RichTextBodyOption } from './body/serializeBody'\nimport { makeRenderBody } from './body/serializeBody'\nimport type { ActionRunArgs } from './defineAction'\nimport type { ActionRegistry } from './registry'\n\n/** A stored action instance from the form's `actions` blocks array. */\nexport type ActionInstance = { blockType: string; [key: string]: unknown }\n\nexport type ActionResult = { type: string; ok: boolean; error?: string }\n\nexport type RunActionsArgs = Omit<ActionRunArgs, 'config' | 'renderBody'> & {\n\tactions: ActionInstance[]\n\tregistry: ActionRegistry\n\trichText?: RichTextBodyOption\n}\n\n/**\n * Run each configured action, isolating failures: a throwing action is captured as a failed result\n * and never breaks the others or the caller.\n */\nexport const runActions = async (args: RunActionsArgs): Promise<ActionResult[]> => {\n\tconst { actions, registry, richText, ...ctx } = args\n\tconst renderBody = makeRenderBody({ values: ctx.
|
|
1
|
+
{"version":3,"file":"runActions.js","names":[],"sources":["../../src/actions/runActions.ts"],"sourcesContent":["import type { RichTextBodyOption } from './body/serializeBody'\nimport { makeRenderBody } from './body/serializeBody'\nimport type { ActionRunArgs } from './defineAction'\nimport type { ActionRegistry } from './registry'\n\n/** A stored action instance from the form's `actions` blocks array. */\nexport type ActionInstance = { blockType: string; [key: string]: unknown }\n\nexport type ActionResult = { type: string; ok: boolean; error?: string }\n\nexport type RunActionsArgs = Omit<ActionRunArgs, 'config' | 'renderBody'> & {\n\tactions: ActionInstance[]\n\tregistry: ActionRegistry\n\trichText?: RichTextBodyOption\n}\n\n/**\n * Run each configured action, isolating failures: a throwing action is captured as a failed result\n * and never breaks the others or the caller.\n */\nexport const runActions = async (args: RunActionsArgs): Promise<ActionResult[]> => {\n\tconst { actions, registry, richText, ...ctx } = args\n\tconst renderBody = makeRenderBody({\n\t\tvalues: ctx.values,\n\t\tdescriptors: ctx.descriptors,\n\t\tform: ctx.form,\n\t\treq: ctx.req,\n\t\trichText,\n\t})\n\tconst results: ActionResult[] = []\n\tfor (const instance of actions) {\n\t\tconst definition = registry.get(instance.blockType)\n\t\tif (!definition) {\n\t\t\tcontinue\n\t\t}\n\t\ttry {\n\t\t\tawait definition.run({ ...ctx, renderBody, config: instance })\n\t\t\tresults.push({ type: instance.blockType, ok: true })\n\t\t} catch (error) {\n\t\t\tresults.push({\n\t\t\t\ttype: instance.blockType,\n\t\t\t\tok: false,\n\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t})\n\t\t}\n\t}\n\treturn results\n}\n"],"mappings":";;;;;;AAoBA,MAAa,aAAa,OAAO,SAAkD;CAClF,MAAM,EAAE,SAAS,UAAU,UAAU,GAAG,QAAQ;CAChD,MAAM,aAAa,eAAe;EACjC,QAAQ,IAAI;EACZ,aAAa,IAAI;EACjB,MAAM,IAAI;EACV,KAAK,IAAI;EACT;CACD,CAAC;CACD,MAAM,UAA0B,CAAC;CACjC,KAAK,MAAM,YAAY,SAAS;EAC/B,MAAM,aAAa,SAAS,IAAI,SAAS,SAAS;EAClD,IAAI,CAAC,YACJ;EAED,IAAI;GACH,MAAM,WAAW,IAAI;IAAE,GAAG;IAAK;IAAY,QAAQ;GAAS,CAAC;GAC7D,QAAQ,KAAK;IAAE,MAAM,SAAS;IAAW,IAAI;GAAK,CAAC;EACpD,SAAS,OAAO;GACf,QAAQ,KAAK;IACZ,MAAM,SAAS;IACf,IAAI;IACJ,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GAC7D,CAAC;EACF;CACD;CACA,OAAO;AACR"}
|
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"}
|