@10x-media/form-builder 0.1.0-beta.5 → 0.1.0-beta.7
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 +102 -0
- package/dist/actions/buildActionBlocks.js +12 -9
- package/dist/actions/buildActionBlocks.js.map +1 -1
- package/dist/actions/builtin/confirmation.js +27 -79
- package/dist/actions/builtin/confirmation.js.map +1 -1
- package/dist/actions/builtin/emailAction.d.ts +17 -0
- package/dist/actions/builtin/emailAction.js +83 -0
- package/dist/actions/builtin/emailAction.js.map +1 -0
- package/dist/actions/builtin/emailTeam.js +14 -84
- package/dist/actions/builtin/emailTeam.js.map +1 -1
- package/dist/actions/builtin/index.d.ts +9 -11
- package/dist/actions/builtin/index.js +11 -11
- package/dist/actions/builtin/index.js.map +1 -1
- package/dist/actions/defineAction.d.ts +2 -2
- package/dist/actions/defineAction.js.map +1 -1
- package/dist/actions/emailRecipients.d.ts +12 -0
- package/dist/actions/emailRecipients.js +116 -0
- package/dist/actions/emailRecipients.js.map +1 -0
- package/dist/actions/registry.d.ts +2 -1
- package/dist/actions/registry.js +2 -6
- package/dist/actions/registry.js.map +1 -1
- package/dist/actions/task.js +5 -3
- package/dist/actions/task.js.map +1 -1
- package/dist/calc/evaluate.js +1 -2
- package/dist/calc/evaluate.js.map +1 -1
- package/dist/calc/normalizeCalc.js +1 -2
- package/dist/calc/normalizeCalc.js.map +1 -1
- package/dist/calc/types.js.map +1 -1
- package/dist/client/ConsentBlockLabel.d.ts +15 -0
- package/dist/client/ConsentBlockLabel.js +71 -0
- package/dist/client/ConsentBlockLabel.js.map +1 -0
- package/dist/client/ConsentSourceRowLabel.d.ts +9 -0
- package/dist/client/ConsentSourceRowLabel.js +21 -0
- package/dist/client/ConsentSourceRowLabel.js.map +1 -0
- package/dist/client/EndpointOptionsSelect.d.ts +1 -0
- package/dist/client/EndpointOptionsSelect.js +4 -1
- package/dist/client/EndpointOptionsSelect.js.map +1 -1
- package/dist/client/FieldNameSelect.d.ts +1 -0
- package/dist/client/FieldNameSelect.js +4 -1
- package/dist/client/FieldNameSelect.js.map +1 -1
- package/dist/client/RecipientsSelect.d.ts +31 -0
- package/dist/client/RecipientsSelect.js +174 -0
- package/dist/client/RecipientsSelect.js.map +1 -0
- package/dist/client/admin.css +5 -0
- package/dist/collections/buttonFields.js +1 -1
- package/dist/collections/formSubmissions.js +4 -2
- package/dist/collections/formSubmissions.js.map +1 -1
- package/dist/collections/forms.js +46 -99
- package/dist/collections/forms.js.map +1 -1
- package/dist/collections/formsEndpoints.js +101 -0
- package/dist/collections/formsEndpoints.js.map +1 -0
- package/dist/conditions/evaluate.js +1 -1
- package/dist/conditions/evaluate.js.map +1 -1
- package/dist/conditions/fieldTypes.d.ts +3 -1
- package/dist/conditions/fieldTypes.js +4 -1
- package/dist/conditions/fieldTypes.js.map +1 -1
- package/dist/conditions/normalizeConditions.js +18 -5
- package/dist/conditions/normalizeConditions.js.map +1 -1
- package/dist/consent/captureConsent.d.ts +21 -5
- package/dist/consent/captureConsent.js +31 -7
- package/dist/consent/captureConsent.js.map +1 -1
- package/dist/consent/consentSourcesField.d.ts +8 -0
- package/dist/consent/consentSourcesField.js +13 -2
- package/dist/consent/consentSourcesField.js.map +1 -1
- package/dist/consent/resolveConsentEntries.d.ts +1 -1
- package/dist/consent/resolveConsentEntries.js +1 -4
- package/dist/consent/resolveConsentEntries.js.map +1 -1
- package/dist/consent/resolveConsentSourcesRequest.js +0 -1
- package/dist/consent/resolveConsentSourcesRequest.js.map +1 -1
- package/dist/consent/resolveConsentStatements.d.ts +1 -1
- package/dist/consent/resolveConsentStatements.js.map +1 -1
- package/dist/consent/types.d.ts +13 -1
- package/dist/email/departments.d.ts +13 -9
- package/dist/email/departments.js +7 -50
- package/dist/email/departments.js.map +1 -1
- package/dist/email/departmentsField.d.ts +4 -0
- package/dist/email/departmentsField.js +5 -1
- package/dist/email/departmentsField.js.map +1 -1
- package/dist/exports/client.d.ts +4 -1
- package/dist/exports/client.js +4 -1
- package/dist/exports/react.d.ts +3 -2
- package/dist/exports/react.js +4 -3
- package/dist/exports/rsc.js +1 -1
- package/dist/exports/types.d.ts +1 -1
- package/dist/fields/buildFieldBlocks.js +4 -4
- package/dist/fields/buildFieldBlocks.js.map +1 -1
- package/dist/fields/builtin/consent.js +6 -1
- package/dist/fields/builtin/consent.js.map +1 -1
- package/dist/fields/builtin/email.js +1 -1
- package/dist/fields/builtin/email.js.map +1 -1
- package/dist/fields/builtin/file.js +9 -3
- package/dist/fields/builtin/file.js.map +1 -1
- package/dist/fields/builtin/index.d.ts +1 -1
- package/dist/fields/builtin/index.js +3 -3
- package/dist/fields/builtin/index.js.map +1 -1
- package/dist/fields/builtin/message.js +1 -1
- package/dist/fields/builtin/repeater.js +1 -1
- package/dist/fields/builtin/select.js +1 -1
- package/dist/fields/registry.d.ts +2 -1
- package/dist/fields/registry.js +2 -6
- package/dist/fields/registry.js.map +1 -1
- package/dist/fields/sharedConfig.js +1 -1
- package/dist/fields/types.d.ts +9 -2
- package/dist/flow/normalizeFlow.js +7 -3
- package/dist/flow/normalizeFlow.js.map +1 -1
- package/dist/form/toFormDocument.d.ts +20 -3
- package/dist/form/toFormDocument.js +16 -4
- package/dist/form/toFormDocument.js.map +1 -1
- package/dist/form/types.d.ts +5 -5
- package/dist/index.d.ts +5 -193
- package/dist/index.js +18 -8
- package/dist/index.js.map +1 -1
- package/dist/options.d.ts +228 -0
- package/dist/plugin/applyRegistryConfig.d.ts +11 -0
- package/dist/plugin/applyRegistryConfig.js +20 -0
- package/dist/plugin/applyRegistryConfig.js.map +1 -0
- package/dist/plugin/registerCollections.js +3 -1
- package/dist/plugin/registerCollections.js.map +1 -1
- package/dist/plugin/uploadsCollection.d.ts +3 -0
- package/dist/plugin/uploadsCollection.js +10 -1
- package/dist/plugin/uploadsCollection.js.map +1 -1
- package/dist/poll/buildPollOptionSourceFields.js +2 -2
- package/dist/poll/buildPollOptionSourceFields.js.map +1 -1
- package/dist/poll/definePollOptionSource.d.ts +2 -2
- package/dist/poll/definePollOptionSource.js.map +1 -1
- package/dist/poll/pollTypeRegistry.js +2 -4
- package/dist/poll/pollTypeRegistry.js.map +1 -1
- package/dist/poll/registry.d.ts +2 -1
- package/dist/poll/registry.js +2 -6
- package/dist/poll/registry.js.map +1 -1
- package/dist/poll/resolvePollOptionsRequest.js +0 -1
- package/dist/poll/resolvePollOptionsRequest.js.map +1 -1
- package/dist/prefill/valuesFromSearchParams.js +2 -7
- package/dist/prefill/valuesFromSearchParams.js.map +1 -1
- package/dist/react/Form.d.ts +3 -1
- package/dist/react/Form.js +93 -155
- package/dist/react/Form.js.map +1 -1
- package/dist/react/FormContext.d.ts +5 -1
- package/dist/react/FormContext.js.map +1 -1
- package/dist/react/FormFields.d.ts +18 -0
- package/dist/react/FormFields.js +114 -0
- package/dist/react/FormFields.js.map +1 -0
- package/dist/react/FormLayout.d.ts +4 -2
- package/dist/react/FormLayout.js +3 -2
- package/dist/react/FormLayout.js.map +1 -1
- package/dist/react/Poll.js +13 -3
- package/dist/react/Poll.js.map +1 -1
- package/dist/react/presentation/DialogSurface.js +1 -4
- package/dist/react/presentation/DialogSurface.js.map +1 -1
- package/dist/react/renderers/calculation.js +1 -3
- package/dist/react/renderers/calculation.js.map +1 -1
- package/dist/react/renderers/checkbox.js +1 -3
- package/dist/react/renderers/checkbox.js.map +1 -1
- package/dist/react/renderers/consent.js +2 -3
- 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 -3
- package/dist/react/renderers/email.js.map +1 -1
- package/dist/react/renderers/file.js +6 -7
- package/dist/react/renderers/file.js.map +1 -1
- package/dist/react/renderers/number.js +1 -3
- package/dist/react/renderers/number.js.map +1 -1
- package/dist/react/renderers/region.js +1 -3
- package/dist/react/renderers/region.js.map +1 -1
- package/dist/react/renderers/repeater.js +20 -4
- package/dist/react/renderers/repeater.js.map +1 -1
- package/dist/react/renderers/select.js +1 -3
- package/dist/react/renderers/select.js.map +1 -1
- package/dist/react/renderers/text.js +1 -3
- package/dist/react/renderers/text.js.map +1 -1
- package/dist/react/renderers/textarea.js +1 -3
- package/dist/react/renderers/textarea.js.map +1 -1
- package/dist/spam/constants.js +7 -1
- package/dist/spam/constants.js.map +1 -1
- package/dist/spam/resolveSpam.js +2 -1
- package/dist/spam/resolveSpam.js.map +1 -1
- package/dist/spam/spamGuard.js +22 -6
- package/dist/spam/spamGuard.js.map +1 -1
- package/dist/spam/types.d.ts +17 -2
- package/dist/submissions/SubmissionAnswers.js.map +1 -1
- package/dist/submissions/SubmissionAnswersClient.js +33 -26
- package/dist/submissions/SubmissionAnswersClient.js.map +1 -1
- package/dist/submissions/coerceBoolean.js +14 -0
- package/dist/submissions/coerceBoolean.js.map +1 -0
- package/dist/submissions/runSubmission.js +9 -8
- package/dist/submissions/runSubmission.js.map +1 -1
- package/dist/submissions/validateSubmission.js +4 -2
- package/dist/submissions/validateSubmission.js.map +1 -1
- package/dist/translations/de.js +14 -2
- package/dist/translations/de.js.map +1 -1
- package/dist/translations/en.js +14 -2
- package/dist/translations/en.js.map +1 -1
- package/dist/translations/keys.d.ts +14 -2
- package/dist/translations/keys.js +14 -2
- package/dist/translations/keys.js.map +1 -1
- package/dist/translations/server.js +8 -1
- package/dist/translations/server.js.map +1 -1
- package/dist/uploads/captureFileRef.d.ts +10 -3
- package/dist/uploads/captureFileRef.js +18 -9
- package/dist/uploads/captureFileRef.js.map +1 -1
- package/dist/uploads/resolveFileRef.js +2 -1
- package/dist/uploads/resolveFileRef.js.map +1 -1
- package/dist/validation/builtin/email.js +1 -1
- package/dist/validation/builtin/email.js.map +1 -1
- package/dist/validation/builtin/notAlreadySubmitted.js +1 -0
- package/dist/validation/builtin/notAlreadySubmitted.js.map +1 -1
- package/dist/validation/emailPattern.js +10 -0
- package/dist/validation/emailPattern.js.map +1 -0
- package/dist/validation/registry.d.ts +2 -1
- package/dist/validation/registry.js +2 -9
- package/dist/validation/registry.js.map +1 -1
- package/package.json +2 -2
|
@@ -37,7 +37,8 @@ const conditionOperators = {
|
|
|
37
37
|
...EQUALITY,
|
|
38
38
|
...ORDER,
|
|
39
39
|
...EXISTS
|
|
40
|
-
]
|
|
40
|
+
],
|
|
41
|
+
presence: [...EXISTS]
|
|
41
42
|
};
|
|
42
43
|
/** Default condition input for a stored value kind; a field type may override via its `conditionType`. */
|
|
43
44
|
const defaultConditionType = (value) => {
|
|
@@ -46,6 +47,8 @@ const defaultConditionType = (value) => {
|
|
|
46
47
|
case "boolean": return "checkbox";
|
|
47
48
|
case "date": return "date";
|
|
48
49
|
case "text[]": return "select";
|
|
50
|
+
case "file":
|
|
51
|
+
case "repeater": return "presence";
|
|
49
52
|
default: return "text";
|
|
50
53
|
}
|
|
51
54
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fieldTypes.js","names":[],"sources":["../../src/conditions/fieldTypes.ts"],"sourcesContent":["import type { Operator } from 'payload'\nimport type { FormFieldValueKind } from '../fields/types'\n\n/**\n * The condition input families the builder supports. A deliberate subset of Payload's field-condition\n * types, restricted to the operators `evaluateCondition` implements (no geo/relationship in v1).\n */\nexport type ConditionFieldType = 'text' | 'number' | 'select' | 'checkbox' | 'date'\n\nconst EQUALITY: Operator[] = ['equals', 'not_equals']\nconst MEMBERSHIP: Operator[] = ['in', 'not_in']\nconst ORDER: Operator[] = ['greater_than', 'greater_than_equal', 'less_than', 'less_than_equal']\nconst TEXT_MATCH: Operator[] = ['like', 'not_like', 'contains']\nconst EXISTS: Operator[] = ['exists']\n\n/** Operators offered per condition type. Each is implemented by `evaluateCondition`. */\nexport const conditionOperators: Record<ConditionFieldType, Operator[]> = {\n\ttext: [...EQUALITY, ...MEMBERSHIP, ...TEXT_MATCH, ...EXISTS],\n\tnumber: [...EQUALITY, ...MEMBERSHIP, ...ORDER, ...EXISTS],\n\tselect: [...EQUALITY, ...MEMBERSHIP, ...EXISTS],\n\tcheckbox: [...EQUALITY, ...EXISTS],\n\tdate: [...EQUALITY, ...ORDER, ...EXISTS],\n}\n\n/** Default condition input for a stored value kind; a field type may override via its `conditionType`. */\nexport const defaultConditionType = (value: FormFieldValueKind): ConditionFieldType => {\n\tswitch (value) {\n\t\tcase 'number':\n\t\t\treturn 'number'\n\t\tcase 'boolean':\n\t\t\treturn 'checkbox'\n\t\tcase 'date':\n\t\t\treturn 'date'\n\t\tcase 'text[]':\n\t\t\treturn 'select'\n\t\tdefault:\n\t\t\treturn 'text'\n\t}\n}\n\n/**\n * The Payload `operators:*` i18n keys the builder uses for operator labels. A local literal union (not\n * imported from `@payloadcms/translations`, which this package never depends on); each member is a real\n * client translation key, so a value of this type is accepted by Payload's `i18n.t`.\n */\nexport type OperatorLabelKey =\n\t| 'operators:contains'\n\t| 'operators:equals'\n\t| 'operators:exists'\n\t| 'operators:isGreaterThan'\n\t| 'operators:isGreaterThanOrEqualTo'\n\t| 'operators:isIn'\n\t| 'operators:isLessThan'\n\t| 'operators:isLessThanOrEqualTo'\n\t| 'operators:isLike'\n\t| 'operators:isNotEqualTo'\n\t| 'operators:isNotIn'\n\t| 'operators:isNotLike'\n\nconst OPERATOR_LABEL_KEYS: Partial<Record<Operator, OperatorLabelKey>> = {\n\tequals: 'operators:equals',\n\tnot_equals: 'operators:isNotEqualTo',\n\tin: 'operators:isIn',\n\tnot_in: 'operators:isNotIn',\n\texists: 'operators:exists',\n\tgreater_than: 'operators:isGreaterThan',\n\tgreater_than_equal: 'operators:isGreaterThanOrEqualTo',\n\tless_than: 'operators:isLessThan',\n\tless_than_equal: 'operators:isLessThanOrEqualTo',\n\tlike: 'operators:isLike',\n\tnot_like: 'operators:isNotLike',\n\tcontains: 'operators:contains',\n}\n\n/**\n * The i18n key for an operator's label, in Payload's own `operators:*` namespace (always present in the\n * admin), so the builder reads native, localized operator labels without duplicating them in our i18n.\n * Keys mirror Payload's `WhereBuilder/field-types.tsx` operator labels. Every operator surfaced by\n * `conditionOperators` is mapped; the `operators:equals` fallback is unreachable for the offered\n * catalogs (no geo/`all` operator is ever presented) and exists only to keep the return type total.\n */\nexport const operatorLabelKey = (operator: Operator): OperatorLabelKey =>\n\tOPERATOR_LABEL_KEYS[operator] ?? 'operators:equals'\n\n/**\n * The first (default) operator a condition type offers. Every catalog leads with `equals`, so the\n * fallback is unreachable; it exists only to keep the return non-optional under `noUncheckedIndexedAccess`.\n */\nexport const firstOperatorFor = (type: ConditionFieldType): Operator =>\n\tconditionOperators[type][0] ?? 'equals'\n\n/** The value control an operator needs: a boolean toggle (`exists`), a list (`in`/`not_in`), or a scalar. */\nexport const operatorValueShape = (operator: Operator): 'boolean' | 'array' | 'scalar' => {\n\tif (operator === 'exists') {\n\t\treturn 'boolean'\n\t}\n\tif (operator === 'in' || operator === 'not_in') {\n\t\treturn 'array'\n\t}\n\treturn 'scalar'\n}\n"],"mappings":";
|
|
1
|
+
{"version":3,"file":"fieldTypes.js","names":[],"sources":["../../src/conditions/fieldTypes.ts"],"sourcesContent":["import type { Operator } from 'payload'\nimport type { FormFieldValueKind } from '../fields/types'\n\n/**\n * The condition input families the builder supports. A deliberate subset of Payload's field-condition\n * types, restricted to the operators `evaluateCondition` implements (no geo/relationship in v1).\n * `presence` is for values that only support an existence check (a file upload, a repeater), never a\n * scalar comparison.\n */\nexport type ConditionFieldType = 'text' | 'number' | 'select' | 'checkbox' | 'date' | 'presence'\n\nconst EQUALITY: Operator[] = ['equals', 'not_equals']\nconst MEMBERSHIP: Operator[] = ['in', 'not_in']\nconst ORDER: Operator[] = ['greater_than', 'greater_than_equal', 'less_than', 'less_than_equal']\nconst TEXT_MATCH: Operator[] = ['like', 'not_like', 'contains']\nconst EXISTS: Operator[] = ['exists']\n\n/** Operators offered per condition type. Each is implemented by `evaluateCondition`. */\nexport const conditionOperators: Record<ConditionFieldType, Operator[]> = {\n\ttext: [...EQUALITY, ...MEMBERSHIP, ...TEXT_MATCH, ...EXISTS],\n\tnumber: [...EQUALITY, ...MEMBERSHIP, ...ORDER, ...EXISTS],\n\tselect: [...EQUALITY, ...MEMBERSHIP, ...EXISTS],\n\tcheckbox: [...EQUALITY, ...EXISTS],\n\tdate: [...EQUALITY, ...ORDER, ...EXISTS],\n\tpresence: [...EXISTS],\n}\n\n/** Default condition input for a stored value kind; a field type may override via its `conditionType`. */\nexport const defaultConditionType = (value: FormFieldValueKind): ConditionFieldType => {\n\tswitch (value) {\n\t\tcase 'number':\n\t\t\treturn 'number'\n\t\tcase 'boolean':\n\t\t\treturn 'checkbox'\n\t\tcase 'date':\n\t\t\treturn 'date'\n\t\tcase 'text[]':\n\t\t\treturn 'select'\n\t\tcase 'file':\n\t\tcase 'repeater':\n\t\t\t// Non-scalar values: a FileRef object or a row array cannot be compared with text/select\n\t\t\t// operators (they only stringify), so offer presence-only checks.\n\t\t\treturn 'presence'\n\t\tdefault:\n\t\t\treturn 'text'\n\t}\n}\n\n/**\n * The Payload `operators:*` i18n keys the builder uses for operator labels. A local literal union (not\n * imported from `@payloadcms/translations`, which this package never depends on); each member is a real\n * client translation key, so a value of this type is accepted by Payload's `i18n.t`.\n */\nexport type OperatorLabelKey =\n\t| 'operators:contains'\n\t| 'operators:equals'\n\t| 'operators:exists'\n\t| 'operators:isGreaterThan'\n\t| 'operators:isGreaterThanOrEqualTo'\n\t| 'operators:isIn'\n\t| 'operators:isLessThan'\n\t| 'operators:isLessThanOrEqualTo'\n\t| 'operators:isLike'\n\t| 'operators:isNotEqualTo'\n\t| 'operators:isNotIn'\n\t| 'operators:isNotLike'\n\nconst OPERATOR_LABEL_KEYS: Partial<Record<Operator, OperatorLabelKey>> = {\n\tequals: 'operators:equals',\n\tnot_equals: 'operators:isNotEqualTo',\n\tin: 'operators:isIn',\n\tnot_in: 'operators:isNotIn',\n\texists: 'operators:exists',\n\tgreater_than: 'operators:isGreaterThan',\n\tgreater_than_equal: 'operators:isGreaterThanOrEqualTo',\n\tless_than: 'operators:isLessThan',\n\tless_than_equal: 'operators:isLessThanOrEqualTo',\n\tlike: 'operators:isLike',\n\tnot_like: 'operators:isNotLike',\n\tcontains: 'operators:contains',\n}\n\n/**\n * The i18n key for an operator's label, in Payload's own `operators:*` namespace (always present in the\n * admin), so the builder reads native, localized operator labels without duplicating them in our i18n.\n * Keys mirror Payload's `WhereBuilder/field-types.tsx` operator labels. Every operator surfaced by\n * `conditionOperators` is mapped; the `operators:equals` fallback is unreachable for the offered\n * catalogs (no geo/`all` operator is ever presented) and exists only to keep the return type total.\n */\nexport const operatorLabelKey = (operator: Operator): OperatorLabelKey =>\n\tOPERATOR_LABEL_KEYS[operator] ?? 'operators:equals'\n\n/**\n * The first (default) operator a condition type offers. Every catalog leads with `equals`, so the\n * fallback is unreachable; it exists only to keep the return non-optional under `noUncheckedIndexedAccess`.\n */\nexport const firstOperatorFor = (type: ConditionFieldType): Operator =>\n\tconditionOperators[type][0] ?? 'equals'\n\n/** The value control an operator needs: a boolean toggle (`exists`), a list (`in`/`not_in`), or a scalar. */\nexport const operatorValueShape = (operator: Operator): 'boolean' | 'array' | 'scalar' => {\n\tif (operator === 'exists') {\n\t\treturn 'boolean'\n\t}\n\tif (operator === 'in' || operator === 'not_in') {\n\t\treturn 'array'\n\t}\n\treturn 'scalar'\n}\n"],"mappings":";AAWA,MAAM,WAAuB,CAAC,UAAU,YAAY;AACpD,MAAM,aAAyB,CAAC,MAAM,QAAQ;AAC9C,MAAM,QAAoB;CAAC;CAAgB;CAAsB;CAAa;AAAiB;AAC/F,MAAM,aAAyB;CAAC;CAAQ;CAAY;AAAU;AAC9D,MAAM,SAAqB,CAAC,QAAQ;;AAGpC,MAAa,qBAA6D;CACzE,MAAM;EAAC,GAAG;EAAU,GAAG;EAAY,GAAG;EAAY,GAAG;CAAM;CAC3D,QAAQ;EAAC,GAAG;EAAU,GAAG;EAAY,GAAG;EAAO,GAAG;CAAM;CACxD,QAAQ;EAAC,GAAG;EAAU,GAAG;EAAY,GAAG;CAAM;CAC9C,UAAU,CAAC,GAAG,UAAU,GAAG,MAAM;CACjC,MAAM;EAAC,GAAG;EAAU,GAAG;EAAO,GAAG;CAAM;CACvC,UAAU,CAAC,GAAG,MAAM;AACrB;;AAGA,MAAa,wBAAwB,UAAkD;CACtF,QAAQ,OAAR;EACC,KAAK,UACJ,OAAO;EACR,KAAK,WACJ,OAAO;EACR,KAAK,QACJ,OAAO;EACR,KAAK,UACJ,OAAO;EACR,KAAK;EACL,KAAK,YAGJ,OAAO;EACR,SACC,OAAO;CACT;AACD;AAqBA,MAAM,sBAAmE;CACxE,QAAQ;CACR,YAAY;CACZ,IAAI;CACJ,QAAQ;CACR,QAAQ;CACR,cAAc;CACd,oBAAoB;CACpB,WAAW;CACX,iBAAiB;CACjB,MAAM;CACN,UAAU;CACV,UAAU;AACX;;;;;;;;AASA,MAAa,oBAAoB,aAChC,oBAAoB,aAAa;;;;;AAMlC,MAAa,oBAAoB,SAChC,mBAAmB,MAAM,MAAM;;AAGhC,MAAa,sBAAsB,aAAuD;CACzF,IAAI,aAAa,UAChB,OAAO;CAER,IAAI,aAAa,QAAQ,aAAa,UACrC,OAAO;CAER,OAAO;AACR"}
|
|
@@ -12,7 +12,11 @@ const isValidConstraint = (constraint, operandTypes) => {
|
|
|
12
12
|
const operator = Object.keys(ops)[0];
|
|
13
13
|
return Boolean(operator && conditionOperators[type].includes(operator));
|
|
14
14
|
};
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Canonicalize a `Where` to OR-of-ANDs and strip constraints whose operand field is unknown or whose
|
|
17
|
+
* operator is invalid for that field's condition type. Returns undefined when nothing valid remains.
|
|
18
|
+
*/
|
|
19
|
+
const normalizeWhere = (raw, operandTypes) => {
|
|
16
20
|
if (raw == null || typeof raw !== "object" || Object.keys(raw).length === 0) return;
|
|
17
21
|
const canonical = transformWhereQuery(raw);
|
|
18
22
|
const groups = (Array.isArray(canonical.or) ? canonical.or : Array.isArray(canonical.and) ? [{ and: canonical.and }] : []).map((group) => {
|
|
@@ -25,16 +29,25 @@ const normalizeOne = (raw, operandTypes) => {
|
|
|
25
29
|
* to OR-of-ANDs and strips constraints whose operand field is missing or whose operator is invalid for
|
|
26
30
|
* that field's condition type, so stored conditions always match what `evaluateCondition` can run.
|
|
27
31
|
*/
|
|
28
|
-
|
|
32
|
+
/**
|
|
33
|
+
* The operand-name -> condition-type map for a form's fields. A field is a valid condition operand
|
|
34
|
+
* only when it has a name and its block type has a condition type (display-only types are excluded),
|
|
35
|
+
* so a constraint referencing anything else is stripped, matching the client operand list.
|
|
36
|
+
*/
|
|
37
|
+
const buildOperandTypes = (fields, conditionTypes) => {
|
|
29
38
|
const operandTypes = /* @__PURE__ */ new Map();
|
|
30
39
|
for (const row of fields) {
|
|
31
40
|
const name = typeof row.name === "string" ? row.name.trim() : "";
|
|
32
41
|
const conditionType = conditionTypes[row.blockType];
|
|
33
42
|
if (name.length > 0 && conditionType) operandTypes.set(name, conditionType);
|
|
34
43
|
}
|
|
44
|
+
return operandTypes;
|
|
45
|
+
};
|
|
46
|
+
const normalizeFormConditions = (fields, conditionTypes) => {
|
|
47
|
+
const operandTypes = buildOperandTypes(fields, conditionTypes);
|
|
35
48
|
return fields.map((row) => {
|
|
36
|
-
const visibleWhen =
|
|
37
|
-
const validateWhen =
|
|
49
|
+
const visibleWhen = normalizeWhere(row.visibleWhen, operandTypes);
|
|
50
|
+
const validateWhen = normalizeWhere(row.validateWhen, operandTypes);
|
|
38
51
|
return {
|
|
39
52
|
...row,
|
|
40
53
|
visibleWhen,
|
|
@@ -43,6 +56,6 @@ const normalizeFormConditions = (fields, conditionTypes) => {
|
|
|
43
56
|
});
|
|
44
57
|
};
|
|
45
58
|
//#endregion
|
|
46
|
-
export { normalizeFormConditions };
|
|
59
|
+
export { buildOperandTypes, normalizeFormConditions, normalizeWhere };
|
|
47
60
|
|
|
48
61
|
//# sourceMappingURL=normalizeConditions.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"normalizeConditions.js","names":[],"sources":["../../src/conditions/normalizeConditions.ts"],"sourcesContent":["import type { Where } from 'payload'\nimport { transformWhereQuery } from 'payload/shared'\nimport type { ConditionFieldType } from './fieldTypes'\nimport { conditionOperators } from './fieldTypes'\n\nexport type FieldRow = {\n\tblockType: string\n\tname?: string\n\tvisibleWhen?: unknown\n\tvalidateWhen?: unknown\n\t[key: string]: unknown\n}\n\nconst isValidConstraint = (\n\tconstraint: unknown,\n\toperandTypes: Map<string, ConditionFieldType>\n): boolean => {\n\tif (constraint == null || typeof constraint !== 'object') {\n\t\treturn false\n\t}\n\tconst field = Object.keys(constraint as object)[0]\n\tif (!field) {\n\t\treturn false\n\t}\n\tconst type = operandTypes.get(field)\n\tif (!type) {\n\t\treturn false\n\t}\n\tconst ops = (constraint as Record<string, unknown>)[field]\n\tif (ops == null || typeof ops !== 'object') {\n\t\treturn false\n\t}\n\tconst operator = Object.keys(ops as object)[0]\n\treturn Boolean(operator && (conditionOperators[type] as readonly string[]).includes(operator))\n}\n\
|
|
1
|
+
{"version":3,"file":"normalizeConditions.js","names":[],"sources":["../../src/conditions/normalizeConditions.ts"],"sourcesContent":["import type { Where } from 'payload'\nimport { transformWhereQuery } from 'payload/shared'\nimport type { ConditionFieldType } from './fieldTypes'\nimport { conditionOperators } from './fieldTypes'\n\nexport type FieldRow = {\n\tblockType: string\n\tname?: string\n\tvisibleWhen?: unknown\n\tvalidateWhen?: unknown\n\t[key: string]: unknown\n}\n\nconst isValidConstraint = (\n\tconstraint: unknown,\n\toperandTypes: Map<string, ConditionFieldType>\n): boolean => {\n\tif (constraint == null || typeof constraint !== 'object') {\n\t\treturn false\n\t}\n\tconst field = Object.keys(constraint as object)[0]\n\tif (!field) {\n\t\treturn false\n\t}\n\tconst type = operandTypes.get(field)\n\tif (!type) {\n\t\treturn false\n\t}\n\tconst ops = (constraint as Record<string, unknown>)[field]\n\tif (ops == null || typeof ops !== 'object') {\n\t\treturn false\n\t}\n\tconst operator = Object.keys(ops as object)[0]\n\treturn Boolean(operator && (conditionOperators[type] as readonly string[]).includes(operator))\n}\n\n/**\n * Canonicalize a `Where` to OR-of-ANDs and strip constraints whose operand field is unknown or whose\n * operator is invalid for that field's condition type. Returns undefined when nothing valid remains.\n */\nexport const normalizeWhere = (\n\traw: unknown,\n\toperandTypes: Map<string, ConditionFieldType>\n): Where | undefined => {\n\tif (raw == null || typeof raw !== 'object' || Object.keys(raw as object).length === 0) {\n\t\treturn undefined\n\t}\n\tconst canonical = transformWhereQuery(raw as Where)\n\tconst or = Array.isArray(canonical.or)\n\t\t? canonical.or\n\t\t: Array.isArray(canonical.and)\n\t\t\t? [{ and: canonical.and }]\n\t\t\t: []\n\tconst groups = or\n\t\t.map((group) => {\n\t\t\tconst and = Array.isArray((group as Where).and) ? ((group as Where).and as unknown[]) : []\n\t\t\treturn and.filter((constraint) => isValidConstraint(constraint, operandTypes))\n\t\t})\n\t\t.filter((group) => group.length > 0)\n\treturn groups.length > 0 ? ({ or: groups.map((and) => ({ and })) } as Where) : undefined\n}\n\n/**\n * Normalize every field's `visibleWhen`/`validateWhen` against the form's own field list. Canonicalizes\n * to OR-of-ANDs and strips constraints whose operand field is missing or whose operator is invalid for\n * that field's condition type, so stored conditions always match what `evaluateCondition` can run.\n */\n/**\n * The operand-name -> condition-type map for a form's fields. A field is a valid condition operand\n * only when it has a name and its block type has a condition type (display-only types are excluded),\n * so a constraint referencing anything else is stripped, matching the client operand list.\n */\nexport const buildOperandTypes = (\n\tfields: FieldRow[],\n\tconditionTypes: Record<string, ConditionFieldType>\n): Map<string, ConditionFieldType> => {\n\tconst operandTypes = new Map<string, ConditionFieldType>()\n\tfor (const row of fields) {\n\t\tconst name = typeof row.name === 'string' ? row.name.trim() : ''\n\t\tconst conditionType = conditionTypes[row.blockType]\n\t\tif (name.length > 0 && conditionType) {\n\t\t\toperandTypes.set(name, conditionType)\n\t\t}\n\t}\n\treturn operandTypes\n}\n\nexport const normalizeFormConditions = (\n\tfields: FieldRow[],\n\tconditionTypes: Record<string, ConditionFieldType>\n): FieldRow[] => {\n\tconst operandTypes = buildOperandTypes(fields, conditionTypes)\n\treturn fields.map((row) => {\n\t\tconst visibleWhen = normalizeWhere(row.visibleWhen, operandTypes)\n\t\tconst validateWhen = normalizeWhere(row.validateWhen, operandTypes)\n\t\treturn { ...row, visibleWhen, validateWhen }\n\t})\n}\n"],"mappings":";;;AAaA,MAAM,qBACL,YACA,iBACa;CACb,IAAI,cAAc,QAAQ,OAAO,eAAe,UAC/C,OAAO;CAER,MAAM,QAAQ,OAAO,KAAK,UAAoB,EAAE;CAChD,IAAI,CAAC,OACJ,OAAO;CAER,MAAM,OAAO,aAAa,IAAI,KAAK;CACnC,IAAI,CAAC,MACJ,OAAO;CAER,MAAM,MAAO,WAAuC;CACpD,IAAI,OAAO,QAAQ,OAAO,QAAQ,UACjC,OAAO;CAER,MAAM,WAAW,OAAO,KAAK,GAAa,EAAE;CAC5C,OAAO,QAAQ,YAAa,mBAAmB,MAA4B,SAAS,QAAQ,CAAC;AAC9F;;;;;AAMA,MAAa,kBACZ,KACA,iBACuB;CACvB,IAAI,OAAO,QAAQ,OAAO,QAAQ,YAAY,OAAO,KAAK,GAAa,EAAE,WAAW,GACnF;CAED,MAAM,YAAY,oBAAoB,GAAY;CAMlD,MAAM,UALK,MAAM,QAAQ,UAAU,EAAE,IAClC,UAAU,KACV,MAAM,QAAQ,UAAU,GAAG,IAC1B,CAAC,EAAE,KAAK,UAAU,IAAI,CAAC,IACvB,CAAC,GAEH,KAAK,UAAU;EAEf,QADY,MAAM,QAAS,MAAgB,GAAG,IAAM,MAAgB,MAAoB,CAAC,GAC9E,QAAQ,eAAe,kBAAkB,YAAY,YAAY,CAAC;CAC9E,CAAC,EACA,QAAQ,UAAU,MAAM,SAAS,CAAC;CACpC,OAAO,OAAO,SAAS,IAAK,EAAE,IAAI,OAAO,KAAK,SAAS,EAAE,IAAI,EAAE,EAAE,IAAc,KAAA;AAChF;;;;;;;;;;;AAYA,MAAa,qBACZ,QACA,mBACqC;CACrC,MAAM,+BAAe,IAAI,IAAgC;CACzD,KAAK,MAAM,OAAO,QAAQ;EACzB,MAAM,OAAO,OAAO,IAAI,SAAS,WAAW,IAAI,KAAK,KAAK,IAAI;EAC9D,MAAM,gBAAgB,eAAe,IAAI;EACzC,IAAI,KAAK,SAAS,KAAK,eACtB,aAAa,IAAI,MAAM,aAAa;CAEtC;CACA,OAAO;AACR;AAEA,MAAa,2BACZ,QACA,mBACgB;CAChB,MAAM,eAAe,kBAAkB,QAAQ,cAAc;CAC7D,OAAO,OAAO,KAAK,QAAQ;EAC1B,MAAM,cAAc,eAAe,IAAI,aAAa,YAAY;EAChE,MAAM,eAAe,eAAe,IAAI,cAAc,YAAY;EAClE,OAAO;GAAE,GAAG;GAAK;GAAa;EAAa;CAC5C,CAAC;AACF"}
|
|
@@ -3,11 +3,16 @@ import { ConsentSourceEntry, ConsentSourcePage } from "./types.js";
|
|
|
3
3
|
import { Payload, PayloadRequest } from "payload";
|
|
4
4
|
|
|
5
5
|
//#region src/consent/captureConsent.d.ts
|
|
6
|
+
/** What a submission snapshots of the agreed wording. `'hash'` and `'text'` combine as `'both'`. */
|
|
7
|
+
type ConsentSnapshotMode = 'hash' | 'text' | 'both' | false;
|
|
6
8
|
type ConsentProof = {
|
|
7
9
|
agreed: boolean; /** The source `id` the field referenced; empty when the author never picked one. */
|
|
8
10
|
source: string;
|
|
9
11
|
page?: ConsentSourcePage;
|
|
10
|
-
versionRef?: string;
|
|
12
|
+
versionRef?: string; /** The source label at submit time, so an audit names the statement even after a rename or delete. */
|
|
13
|
+
name?: string; /** sha256 of the statement plain text agreed to: tamper-evident and PII-free. */
|
|
14
|
+
statementHash?: string; /** Plain-text snapshot of the statement agreed to, for a human-readable audit. */
|
|
15
|
+
statementText?: string;
|
|
11
16
|
at: string;
|
|
12
17
|
};
|
|
13
18
|
/**
|
|
@@ -15,8 +20,18 @@ type ConsentProof = {
|
|
|
15
20
|
* (the field carries an id, never a statement, so there is nothing here the client could forge).
|
|
16
21
|
*
|
|
17
22
|
* Proof is id-based on purpose: it records which document was agreed to, so renaming, re-slugging,
|
|
18
|
-
* or re-routing the policy leaves every past proof intact and resolvable.
|
|
19
|
-
*
|
|
23
|
+
* or re-routing the policy leaves every past proof intact and resolvable. `snapshot` additionally
|
|
24
|
+
* captures the agreed wording (a `statementHash`, the plain `statementText`, or both) plus the source
|
|
25
|
+
* `name`, so an audit shows exactly what was agreed to independent of later edits; `false` keeps the
|
|
26
|
+
* lean id-only proof. The snapshot is captured in every branch, including a refusal and a page-less
|
|
27
|
+
* source.
|
|
28
|
+
*
|
|
29
|
+
* Time-of-check/time-of-use: a visitor reads the wording resolved when the form loaded, but the
|
|
30
|
+
* snapshot here is re-resolved from the source at submit. If the source text changed in that window,
|
|
31
|
+
* the proof records the submit-time wording, not the exact bytes rendered earlier. This is
|
|
32
|
+
* deliberate, the server's live view is authoritative and the window is one visitor session; a form
|
|
33
|
+
* that must freeze the exact wording agreed to should version the source (drafts give a `versionRef`
|
|
34
|
+
* pinning the published version, below).
|
|
20
35
|
*
|
|
21
36
|
* `versionRef` is recorded only when the page's collection has drafts enabled, and then it is the
|
|
22
37
|
* published version document's own id. Versions without drafts get no `versionRef`, because there
|
|
@@ -32,8 +47,9 @@ declare const captureConsent: (args: {
|
|
|
32
47
|
entries: ConsentSourceEntry[];
|
|
33
48
|
payload: Payload;
|
|
34
49
|
req?: PayloadRequest;
|
|
35
|
-
now: string;
|
|
50
|
+
now: string; /** What to snapshot of the agreed wording (plugin option `consent.snapshot`); defaults to `'both'`. */
|
|
51
|
+
snapshot?: ConsentSnapshotMode;
|
|
36
52
|
}) => Promise<ConsentProof>;
|
|
37
53
|
//#endregion
|
|
38
|
-
export { ConsentProof, captureConsent };
|
|
54
|
+
export { ConsentProof, ConsentSnapshotMode, captureConsent };
|
|
39
55
|
//# sourceMappingURL=captureConsent.d.ts.map
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { textOfBody } from "../actions/body/textOfBody.js";
|
|
1
2
|
import { resolvePublishedVersionRef } from "./resolvePublishedVersionRef.js";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
2
4
|
import { hasDraftsEnabled } from "payload/shared";
|
|
3
5
|
//#region src/consent/captureConsent.ts
|
|
4
6
|
/**
|
|
@@ -6,8 +8,18 @@ import { hasDraftsEnabled } from "payload/shared";
|
|
|
6
8
|
* (the field carries an id, never a statement, so there is nothing here the client could forge).
|
|
7
9
|
*
|
|
8
10
|
* Proof is id-based on purpose: it records which document was agreed to, so renaming, re-slugging,
|
|
9
|
-
* or re-routing the policy leaves every past proof intact and resolvable.
|
|
10
|
-
*
|
|
11
|
+
* or re-routing the policy leaves every past proof intact and resolvable. `snapshot` additionally
|
|
12
|
+
* captures the agreed wording (a `statementHash`, the plain `statementText`, or both) plus the source
|
|
13
|
+
* `name`, so an audit shows exactly what was agreed to independent of later edits; `false` keeps the
|
|
14
|
+
* lean id-only proof. The snapshot is captured in every branch, including a refusal and a page-less
|
|
15
|
+
* source.
|
|
16
|
+
*
|
|
17
|
+
* Time-of-check/time-of-use: a visitor reads the wording resolved when the form loaded, but the
|
|
18
|
+
* snapshot here is re-resolved from the source at submit. If the source text changed in that window,
|
|
19
|
+
* the proof records the submit-time wording, not the exact bytes rendered earlier. This is
|
|
20
|
+
* deliberate, the server's live view is authoritative and the window is one visitor session; a form
|
|
21
|
+
* that must freeze the exact wording agreed to should version the source (drafts give a `versionRef`
|
|
22
|
+
* pinning the published version, below).
|
|
11
23
|
*
|
|
12
24
|
* `versionRef` is recorded only when the page's collection has drafts enabled, and then it is the
|
|
13
25
|
* published version document's own id. Versions without drafts get no `versionRef`, because there
|
|
@@ -19,12 +31,24 @@ import { hasDraftsEnabled } from "payload/shared";
|
|
|
19
31
|
*/
|
|
20
32
|
const captureConsent = async (args) => {
|
|
21
33
|
const source = typeof args.field.source === "string" ? args.field.source : "";
|
|
22
|
-
const
|
|
23
|
-
|
|
34
|
+
const entry = source ? args.entries.find((candidate) => candidate.id === source) : void 0;
|
|
35
|
+
const snapshot = args.snapshot ?? "both";
|
|
36
|
+
const withSnapshot = (proof) => {
|
|
37
|
+
if (!entry || snapshot === false) return proof;
|
|
38
|
+
if (typeof entry.name === "string" && entry.name.trim().length > 0) proof.name = entry.name.trim();
|
|
39
|
+
const text = textOfBody(entry.statement);
|
|
40
|
+
if (text.length > 0) {
|
|
41
|
+
if (snapshot === "hash" || snapshot === "both") proof.statementHash = createHash("sha256").update(text).digest("hex");
|
|
42
|
+
if (snapshot === "text" || snapshot === "both") proof.statementText = text;
|
|
43
|
+
}
|
|
44
|
+
return proof;
|
|
45
|
+
};
|
|
46
|
+
const page = entry?.page;
|
|
47
|
+
if (!page) return withSnapshot({
|
|
24
48
|
agreed: args.agreed,
|
|
25
49
|
source,
|
|
26
50
|
at: args.now
|
|
27
|
-
};
|
|
51
|
+
});
|
|
28
52
|
const collection = args.payload.collections[page.relationTo];
|
|
29
53
|
const versionRef = collection && hasDraftsEnabled(collection.config) ? await resolvePublishedVersionRef({
|
|
30
54
|
payload: args.payload,
|
|
@@ -32,7 +56,7 @@ const captureConsent = async (args) => {
|
|
|
32
56
|
id: page.id,
|
|
33
57
|
req: args.req
|
|
34
58
|
}) : null;
|
|
35
|
-
return {
|
|
59
|
+
return withSnapshot({
|
|
36
60
|
agreed: args.agreed,
|
|
37
61
|
source,
|
|
38
62
|
page: {
|
|
@@ -41,7 +65,7 @@ const captureConsent = async (args) => {
|
|
|
41
65
|
},
|
|
42
66
|
...versionRef ? { versionRef } : {},
|
|
43
67
|
at: args.now
|
|
44
|
-
};
|
|
68
|
+
});
|
|
45
69
|
};
|
|
46
70
|
//#endregion
|
|
47
71
|
export { captureConsent };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"captureConsent.js","names":[],"sources":["../../src/consent/captureConsent.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { hasDraftsEnabled } from 'payload/shared'\nimport type { FormFieldInstance } from '../submissions/types'\nimport { resolvePublishedVersionRef } from './resolvePublishedVersionRef'\nimport type { ConsentSourceEntry, ConsentSourcePage } from './types'\n\nexport type ConsentProof = {\n\tagreed: boolean\n\t/** The source `id` the field referenced; empty when the author never picked one. */\n\tsource: string\n\tpage?: ConsentSourcePage\n\tversionRef?: string\n\tat: string\n}\n\n/**\n * The authoritative consent proof, built at submit time from the server's own view of the source\n * (the field carries an id, never a statement, so there is nothing here the client could forge).\n *\n * Proof is id-based on purpose: it records which document was agreed to, so renaming, re-slugging,\n * or re-routing the policy leaves every past proof intact and resolvable. The
|
|
1
|
+
{"version":3,"file":"captureConsent.js","names":[],"sources":["../../src/consent/captureConsent.ts"],"sourcesContent":["import { createHash } from 'node:crypto'\nimport type { Payload, PayloadRequest } from 'payload'\nimport { hasDraftsEnabled } from 'payload/shared'\nimport { textOfBody } from '../actions/body/textOfBody'\nimport type { FormFieldInstance } from '../submissions/types'\nimport { resolvePublishedVersionRef } from './resolvePublishedVersionRef'\nimport type { ConsentSourceEntry, ConsentSourcePage } from './types'\n\n/** What a submission snapshots of the agreed wording. `'hash'` and `'text'` combine as `'both'`. */\nexport type ConsentSnapshotMode = 'hash' | 'text' | 'both' | false\n\nexport type ConsentProof = {\n\tagreed: boolean\n\t/** The source `id` the field referenced; empty when the author never picked one. */\n\tsource: string\n\tpage?: ConsentSourcePage\n\tversionRef?: string\n\t/** The source label at submit time, so an audit names the statement even after a rename or delete. */\n\tname?: string\n\t/** sha256 of the statement plain text agreed to: tamper-evident and PII-free. */\n\tstatementHash?: string\n\t/** Plain-text snapshot of the statement agreed to, for a human-readable audit. */\n\tstatementText?: string\n\tat: string\n}\n\n/**\n * The authoritative consent proof, built at submit time from the server's own view of the source\n * (the field carries an id, never a statement, so there is nothing here the client could forge).\n *\n * Proof is id-based on purpose: it records which document was agreed to, so renaming, re-slugging,\n * or re-routing the policy leaves every past proof intact and resolvable. `snapshot` additionally\n * captures the agreed wording (a `statementHash`, the plain `statementText`, or both) plus the source\n * `name`, so an audit shows exactly what was agreed to independent of later edits; `false` keeps the\n * lean id-only proof. The snapshot is captured in every branch, including a refusal and a page-less\n * source.\n *\n * Time-of-check/time-of-use: a visitor reads the wording resolved when the form loaded, but the\n * snapshot here is re-resolved from the source at submit. If the source text changed in that window,\n * the proof records the submit-time wording, not the exact bytes rendered earlier. This is\n * deliberate, the server's live view is authoritative and the window is one visitor session; a form\n * that must freeze the exact wording agreed to should version the source (drafts give a `versionRef`\n * pinning the published version, below).\n *\n * `versionRef` is recorded only when the page's collection has drafts enabled, and then it is the\n * published version document's own id. Versions without drafts get no `versionRef`, because there\n * is no published/draft distinction to pin to: `_status` only exists under drafts, so a\n * published-version lookup there matches nothing, and recording that as a version reference would\n * be inventing one. Absent means absent, in both cases.\n *\n * `now` is injected by the caller (`new Date().toISOString()`) for testability.\n */\nexport const captureConsent = async (args: {\n\tfield: FormFieldInstance\n\tagreed: boolean\n\t/** The host's resolved sources for this request (see `resolveConsentEntries`). */\n\tentries: ConsentSourceEntry[]\n\tpayload: Payload\n\treq?: PayloadRequest\n\tnow: string\n\t/** What to snapshot of the agreed wording (plugin option `consent.snapshot`); defaults to `'both'`. */\n\tsnapshot?: ConsentSnapshotMode\n}): Promise<ConsentProof> => {\n\tconst source = typeof args.field.source === 'string' ? args.field.source : ''\n\tconst entry = source ? args.entries.find((candidate) => candidate.id === source) : undefined\n\tconst snapshot = args.snapshot ?? 'both'\n\n\t// Capture the agreed wording (and the source label) onto every proof branch. Kept out of the two\n\t// returns so a refusal and a page-less source are snapshotted the same as the happy path.\n\tconst withSnapshot = (proof: ConsentProof): ConsentProof => {\n\t\tif (!entry || snapshot === false) {\n\t\t\treturn proof\n\t\t}\n\t\tif (typeof entry.name === 'string' && entry.name.trim().length > 0) {\n\t\t\tproof.name = entry.name.trim()\n\t\t}\n\t\tconst text = textOfBody(entry.statement)\n\t\tif (text.length > 0) {\n\t\t\tif (snapshot === 'hash' || snapshot === 'both') {\n\t\t\t\tproof.statementHash = createHash('sha256').update(text).digest('hex')\n\t\t\t}\n\t\t\tif (snapshot === 'text' || snapshot === 'both') {\n\t\t\t\tproof.statementText = text\n\t\t\t}\n\t\t}\n\t\treturn proof\n\t}\n\n\tconst page = entry?.page\n\tif (!page) {\n\t\treturn withSnapshot({ agreed: args.agreed, source, at: args.now })\n\t}\n\t// `page.relationTo` is a runtime string; cast the key so the lookup type-checks against a host's\n\t// narrowed collection map (a consumer's generated `CollectionSlug`) as well as the plugin's own\n\t// broad one. The value is still guarded below, since an unregistered slug resolves to undefined.\n\tconst collection = args.payload.collections[page.relationTo as keyof Payload['collections']]\n\tconst versionRef =\n\t\tcollection && hasDraftsEnabled(collection.config)\n\t\t\t? await resolvePublishedVersionRef({\n\t\t\t\t\tpayload: args.payload,\n\t\t\t\t\tcollection: page.relationTo,\n\t\t\t\t\tid: page.id,\n\t\t\t\t\treq: args.req,\n\t\t\t\t})\n\t\t\t: null\n\treturn withSnapshot({\n\t\tagreed: args.agreed,\n\t\tsource,\n\t\tpage: { relationTo: page.relationTo, id: page.id },\n\t\t...(versionRef ? { versionRef } : {}),\n\t\tat: args.now,\n\t})\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,MAAa,iBAAiB,OAAO,SAUR;CAC5B,MAAM,SAAS,OAAO,KAAK,MAAM,WAAW,WAAW,KAAK,MAAM,SAAS;CAC3E,MAAM,QAAQ,SAAS,KAAK,QAAQ,MAAM,cAAc,UAAU,OAAO,MAAM,IAAI,KAAA;CACnF,MAAM,WAAW,KAAK,YAAY;CAIlC,MAAM,gBAAgB,UAAsC;EAC3D,IAAI,CAAC,SAAS,aAAa,OAC1B,OAAO;EAER,IAAI,OAAO,MAAM,SAAS,YAAY,MAAM,KAAK,KAAK,EAAE,SAAS,GAChE,MAAM,OAAO,MAAM,KAAK,KAAK;EAE9B,MAAM,OAAO,WAAW,MAAM,SAAS;EACvC,IAAI,KAAK,SAAS,GAAG;GACpB,IAAI,aAAa,UAAU,aAAa,QACvC,MAAM,gBAAgB,WAAW,QAAQ,EAAE,OAAO,IAAI,EAAE,OAAO,KAAK;GAErE,IAAI,aAAa,UAAU,aAAa,QACvC,MAAM,gBAAgB;EAExB;EACA,OAAO;CACR;CAEA,MAAM,OAAO,OAAO;CACpB,IAAI,CAAC,MACJ,OAAO,aAAa;EAAE,QAAQ,KAAK;EAAQ;EAAQ,IAAI,KAAK;CAAI,CAAC;CAKlE,MAAM,aAAa,KAAK,QAAQ,YAAY,KAAK;CACjD,MAAM,aACL,cAAc,iBAAiB,WAAW,MAAM,IAC7C,MAAM,2BAA2B;EACjC,SAAS,KAAK;EACd,YAAY,KAAK;EACjB,IAAI,KAAK;EACT,KAAK,KAAK;CACX,CAAC,IACA;CACJ,OAAO,aAAa;EACnB,QAAQ,KAAK;EACb;EACA,MAAM;GAAE,YAAY,KAAK;GAAY,IAAI,KAAK;EAAG;EACjD,GAAI,aAAa,EAAE,WAAW,IAAI,CAAC;EACnC,IAAI,KAAK;CACV,CAAC;AACF"}
|
|
@@ -51,6 +51,14 @@ type ConsentSourcesFieldOptions = {
|
|
|
51
51
|
* },
|
|
52
52
|
* })
|
|
53
53
|
* ```
|
|
54
|
+
*
|
|
55
|
+
* The resolver receives the whole form document, so derive tenant scoping from a field on it rather
|
|
56
|
+
* than reading the form back: reading the `forms` collection from the resolver re-enters this form's
|
|
57
|
+
* afterRead hook (the plugin guards against the resulting recursion, but the read is still wasted).
|
|
58
|
+
*
|
|
59
|
+
* A resolver reading its settings document must not thread the save's `req` into a `locale: 'all'`
|
|
60
|
+
* read from within a form's save validation: that flips `req.locale` to `all` and corrupts the
|
|
61
|
+
* localized write of the document being saved. It reads already-committed data, so no `req` is needed.
|
|
54
62
|
*/
|
|
55
63
|
declare const consentSourcesField: (options?: ConsentSourcesFieldOptions) => ArrayField;
|
|
56
64
|
//#endregion
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { localizedIf } from "../fields/localizedIf.js";
|
|
2
1
|
import { keys } from "../translations/keys.js";
|
|
3
2
|
import { labelForKey } from "../translations/server.js";
|
|
3
|
+
import { localizedIf } from "../fields/localizedIf.js";
|
|
4
4
|
//#region src/consent/consentSourcesField.ts
|
|
5
5
|
/**
|
|
6
6
|
* The consent sources array, for the host to place on any collection or global they own: a
|
|
@@ -35,6 +35,14 @@ import { labelForKey } from "../translations/server.js";
|
|
|
35
35
|
* },
|
|
36
36
|
* })
|
|
37
37
|
* ```
|
|
38
|
+
*
|
|
39
|
+
* The resolver receives the whole form document, so derive tenant scoping from a field on it rather
|
|
40
|
+
* than reading the form back: reading the `forms` collection from the resolver re-enters this form's
|
|
41
|
+
* afterRead hook (the plugin guards against the resulting recursion, but the read is still wasted).
|
|
42
|
+
*
|
|
43
|
+
* A resolver reading its settings document must not thread the save's `req` into a `locale: 'all'`
|
|
44
|
+
* read from within a form's save validation: that flips `req.locale` to `all` and corrupts the
|
|
45
|
+
* localized write of the document being saved. It reads already-committed data, so no `req` is needed.
|
|
38
46
|
*/
|
|
39
47
|
const consentSourcesField = (options = {}) => {
|
|
40
48
|
const localize = options.localized !== false;
|
|
@@ -54,7 +62,10 @@ const consentSourcesField = (options = {}) => {
|
|
|
54
62
|
singular: labelForKey(keys.consentSourceSingular),
|
|
55
63
|
plural: labelForKey(keys.consentSourcePlural)
|
|
56
64
|
},
|
|
57
|
-
admin: {
|
|
65
|
+
admin: {
|
|
66
|
+
description: labelForKey(keys.consentSourcesFieldDescription),
|
|
67
|
+
components: { RowLabel: "@10x-media/form-builder/client#ConsentSourceRowLabel" }
|
|
68
|
+
},
|
|
58
69
|
fields: [
|
|
59
70
|
{
|
|
60
71
|
name: "name",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consentSourcesField.js","names":[],"sources":["../../src/consent/consentSourcesField.ts"],"sourcesContent":["import type { ArrayField, CollectionSlug, Field, RichTextField } from 'payload'\nimport { localizedIf } from '../fields/localizedIf'\nimport { keys } from '../translations/keys'\nimport { labelForKey } from '../translations/server'\n\nexport type ConsentSourcesFieldOptions = {\n\t/** Field name, i.e. what the row array is stored under. Defaults to `consentSources`. */\n\tname?: string\n\t/** Field label. Defaults to the plugin's translated one. */\n\tlabel?: ArrayField['label']\n\t/**\n\t * Collections whose documents can be picked as a source's policy page. Omitted (the default):\n\t * no page picker at all, so sources are statement-only and their proofs carry no page or\n\t * version reference. Always stored polymorphically, even for a single slug (see below).\n\t */\n\trelationTo?: CollectionSlug | CollectionSlug[]\n\t/**\n\t * Whether the visitor-facing `statement` and `name` carry `localized: true`. Default `true`;\n\t * Payload strips the flag on hosts without `localization`, so it is safe either way. Mirrors\n\t * the plugin's `localizeContent` option, which this host-called factory cannot see.\n\t */\n\tlocalized?: boolean\n\t/** Overrides the project's default richText editor for the `statement`, as `richText.editor` does plugin-side. */\n\teditor?: RichTextField['editor']\n}\n\n/**\n * The consent sources array, for the host to place on any collection or global they own: a\n * settings global, a tenants collection, a legal-pages parent, wherever the sources belong. The\n * plugin never registers it and never guesses where it lives; a matching `consent.sources` resolver\n * reads it back (see {@link ConsentSourcesResolver}), which is also where multi-tenant scoping goes:\n * place this on the tenant-scoped document and have the resolver return only the sources of the\n * tenant it derives from `req`.\n *\n * A row is a `name` (shown when picking the source on a form, and used as the policy link text\n * beside the statement), the `statement` the visitor agrees to, and optionally the `page` that\n * statement belongs to. The stable reference a consent field stores, and the only part that must\n * outlive edits, is the row's own auto-assigned `id`: it survives a `name` edit or reordering,\n * unlike a hand-authored key would. Authors fill in no version, no URL, and no document id by\n * hand: the version is detected and recorded at submit time, and the page is a picker.\n *\n * `page` is always polymorphic, even when `relationTo` names a single collection, because a\n * monomorphic relationship stores a bare id: the proof needs the collection alongside it to stay\n * resolvable, and a host adding a second collection later would otherwise change the stored shape.\n *\n * ```ts\n * // The host's own collection or global:\n * fields: [consentSourcesField({ relationTo: ['pages', 'legal-notices'] })]\n *\n * // The plugin, reading it back:\n * formBuilder({\n * consent: {\n * sources: async ({ req }) => {\n * const settings = await req.payload.findGlobal({ slug: 'settings', depth: 0, locale: req.locale, req })\n * return (settings.consentSources ?? []).map((row) => ({ ... }))\n * },\n * },\n * })\n * ```\n */\nexport const consentSourcesField = (options: ConsentSourcesFieldOptions = {}): ArrayField => {\n\tconst localize = options.localized !== false\n\t// An empty `relationTo` array is treated as \"no page picker\", not a relationship field pointing\n\t// at nothing (which Payload rejects at boot).\n\tconst relationTo =\n\t\toptions.relationTo && (!Array.isArray(options.relationTo) || options.relationTo.length > 0)\n\t\t\t? options.relationTo\n\t\t\t: undefined\n\tconst pageField: Field[] = relationTo\n\t\t? [\n\t\t\t\t{\n\t\t\t\t\tname: 'page',\n\t\t\t\t\ttype: 'relationship',\n\t\t\t\t\trelationTo: Array.isArray(relationTo) ? relationTo : [relationTo],\n\t\t\t\t\tlabel: labelForKey(keys.consentSourcePage),\n\t\t\t\t\tadmin: { description: labelForKey(keys.consentSourcePageDescription) },\n\t\t\t\t},\n\t\t\t]\n\t\t: []\n\n\treturn {\n\t\tname: options.name ?? 'consentSources',\n\t\ttype: 'array',\n\t\tlabel: options.label ?? labelForKey(keys.consentSourcesField),\n\t\tlabels: {\n\t\t\tsingular: labelForKey(keys.consentSourceSingular),\n\t\t\tplural: labelForKey(keys.consentSourcePlural),\n\t\t},\n\t\tadmin: {
|
|
1
|
+
{"version":3,"file":"consentSourcesField.js","names":[],"sources":["../../src/consent/consentSourcesField.ts"],"sourcesContent":["import type { ArrayField, CollectionSlug, Field, RichTextField } from 'payload'\nimport { localizedIf } from '../fields/localizedIf'\nimport { keys } from '../translations/keys'\nimport { labelForKey } from '../translations/server'\n\nexport type ConsentSourcesFieldOptions = {\n\t/** Field name, i.e. what the row array is stored under. Defaults to `consentSources`. */\n\tname?: string\n\t/** Field label. Defaults to the plugin's translated one. */\n\tlabel?: ArrayField['label']\n\t/**\n\t * Collections whose documents can be picked as a source's policy page. Omitted (the default):\n\t * no page picker at all, so sources are statement-only and their proofs carry no page or\n\t * version reference. Always stored polymorphically, even for a single slug (see below).\n\t */\n\trelationTo?: CollectionSlug | CollectionSlug[]\n\t/**\n\t * Whether the visitor-facing `statement` and `name` carry `localized: true`. Default `true`;\n\t * Payload strips the flag on hosts without `localization`, so it is safe either way. Mirrors\n\t * the plugin's `localizeContent` option, which this host-called factory cannot see.\n\t */\n\tlocalized?: boolean\n\t/** Overrides the project's default richText editor for the `statement`, as `richText.editor` does plugin-side. */\n\teditor?: RichTextField['editor']\n}\n\n/**\n * The consent sources array, for the host to place on any collection or global they own: a\n * settings global, a tenants collection, a legal-pages parent, wherever the sources belong. The\n * plugin never registers it and never guesses where it lives; a matching `consent.sources` resolver\n * reads it back (see {@link ConsentSourcesResolver}), which is also where multi-tenant scoping goes:\n * place this on the tenant-scoped document and have the resolver return only the sources of the\n * tenant it derives from `req`.\n *\n * A row is a `name` (shown when picking the source on a form, and used as the policy link text\n * beside the statement), the `statement` the visitor agrees to, and optionally the `page` that\n * statement belongs to. The stable reference a consent field stores, and the only part that must\n * outlive edits, is the row's own auto-assigned `id`: it survives a `name` edit or reordering,\n * unlike a hand-authored key would. Authors fill in no version, no URL, and no document id by\n * hand: the version is detected and recorded at submit time, and the page is a picker.\n *\n * `page` is always polymorphic, even when `relationTo` names a single collection, because a\n * monomorphic relationship stores a bare id: the proof needs the collection alongside it to stay\n * resolvable, and a host adding a second collection later would otherwise change the stored shape.\n *\n * ```ts\n * // The host's own collection or global:\n * fields: [consentSourcesField({ relationTo: ['pages', 'legal-notices'] })]\n *\n * // The plugin, reading it back:\n * formBuilder({\n * consent: {\n * sources: async ({ req }) => {\n * const settings = await req.payload.findGlobal({ slug: 'settings', depth: 0, locale: req.locale, req })\n * return (settings.consentSources ?? []).map((row) => ({ ... }))\n * },\n * },\n * })\n * ```\n *\n * The resolver receives the whole form document, so derive tenant scoping from a field on it rather\n * than reading the form back: reading the `forms` collection from the resolver re-enters this form's\n * afterRead hook (the plugin guards against the resulting recursion, but the read is still wasted).\n *\n * A resolver reading its settings document must not thread the save's `req` into a `locale: 'all'`\n * read from within a form's save validation: that flips `req.locale` to `all` and corrupts the\n * localized write of the document being saved. It reads already-committed data, so no `req` is needed.\n */\nexport const consentSourcesField = (options: ConsentSourcesFieldOptions = {}): ArrayField => {\n\tconst localize = options.localized !== false\n\t// An empty `relationTo` array is treated as \"no page picker\", not a relationship field pointing\n\t// at nothing (which Payload rejects at boot).\n\tconst relationTo =\n\t\toptions.relationTo && (!Array.isArray(options.relationTo) || options.relationTo.length > 0)\n\t\t\t? options.relationTo\n\t\t\t: undefined\n\tconst pageField: Field[] = relationTo\n\t\t? [\n\t\t\t\t{\n\t\t\t\t\tname: 'page',\n\t\t\t\t\ttype: 'relationship',\n\t\t\t\t\trelationTo: Array.isArray(relationTo) ? relationTo : [relationTo],\n\t\t\t\t\tlabel: labelForKey(keys.consentSourcePage),\n\t\t\t\t\tadmin: { description: labelForKey(keys.consentSourcePageDescription) },\n\t\t\t\t},\n\t\t\t]\n\t\t: []\n\n\treturn {\n\t\tname: options.name ?? 'consentSources',\n\t\ttype: 'array',\n\t\tlabel: options.label ?? labelForKey(keys.consentSourcesField),\n\t\tlabels: {\n\t\t\tsingular: labelForKey(keys.consentSourceSingular),\n\t\t\tplural: labelForKey(keys.consentSourcePlural),\n\t\t},\n\t\tadmin: {\n\t\t\tdescription: labelForKey(keys.consentSourcesFieldDescription),\n\t\t\tcomponents: { RowLabel: '@10x-media/form-builder/client#ConsentSourceRowLabel' },\n\t\t},\n\t\tfields: [\n\t\t\t{\n\t\t\t\tname: 'name',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelForKey(keys.consentSourceLabel),\n\t\t\t\t...localizedIf(localize),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'statement',\n\t\t\t\ttype: 'richText',\n\t\t\t\tlabel: labelForKey(keys.consentSourceStatement),\n\t\t\t\t...(options.editor ? { editor: options.editor } : {}),\n\t\t\t\t...localizedIf(localize),\n\t\t\t},\n\t\t\t...pageField,\n\t\t],\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoEA,MAAa,uBAAuB,UAAsC,CAAC,MAAkB;CAC5F,MAAM,WAAW,QAAQ,cAAc;CAGvC,MAAM,aACL,QAAQ,eAAe,CAAC,MAAM,QAAQ,QAAQ,UAAU,KAAK,QAAQ,WAAW,SAAS,KACtF,QAAQ,aACR,KAAA;CACJ,MAAM,YAAqB,aACxB,CACA;EACC,MAAM;EACN,MAAM;EACN,YAAY,MAAM,QAAQ,UAAU,IAAI,aAAa,CAAC,UAAU;EAChE,OAAO,YAAY,KAAK,iBAAiB;EACzC,OAAO,EAAE,aAAa,YAAY,KAAK,4BAA4B,EAAE;CACtE,CACD,IACC,CAAC;CAEJ,OAAO;EACN,MAAM,QAAQ,QAAQ;EACtB,MAAM;EACN,OAAO,QAAQ,SAAS,YAAY,KAAK,mBAAmB;EAC5D,QAAQ;GACP,UAAU,YAAY,KAAK,qBAAqB;GAChD,QAAQ,YAAY,KAAK,mBAAmB;EAC7C;EACA,OAAO;GACN,aAAa,YAAY,KAAK,8BAA8B;GAC5D,YAAY,EAAE,UAAU,uDAAuD;EAChF;EACA,QAAQ;GACP;IACC,MAAM;IACN,MAAM;IACN,OAAO,YAAY,KAAK,kBAAkB;IAC1C,GAAG,YAAY,QAAQ;GACxB;GACA;IACC,MAAM;IACN,MAAM;IACN,OAAO,YAAY,KAAK,sBAAsB;IAC9C,GAAI,QAAQ,SAAS,EAAE,QAAQ,QAAQ,OAAO,IAAI,CAAC;IACnD,GAAG,YAAY,QAAQ;GACxB;GACA,GAAG;EACJ;CACD;AACD"}
|
|
@@ -4,7 +4,7 @@ import { Payload, PayloadRequest } from "payload";
|
|
|
4
4
|
//#region src/consent/resolveConsentEntries.d.ts
|
|
5
5
|
type ResolveConsentEntriesArgs = {
|
|
6
6
|
payload: Payload;
|
|
7
|
-
req: PayloadRequest; /** The form
|
|
7
|
+
req: PayloadRequest; /** The whole form being resolved; forwarded to the resolver, which casts for non-standard fields (e.g. tenant). */
|
|
8
8
|
form: {
|
|
9
9
|
id: number | string;
|
|
10
10
|
title?: string | null;
|
|
@@ -29,10 +29,7 @@ const resolveConsentEntries = async (args) => {
|
|
|
29
29
|
if (cached) return cached;
|
|
30
30
|
const entries = await resolver({
|
|
31
31
|
req,
|
|
32
|
-
form
|
|
33
|
-
id: form.id,
|
|
34
|
-
title: typeof form.title === "string" ? form.title : void 0
|
|
35
|
-
}
|
|
32
|
+
form
|
|
36
33
|
});
|
|
37
34
|
cache?.set(cacheKey, entries);
|
|
38
35
|
return entries;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolveConsentEntries.js","names":[],"sources":["../../src/consent/resolveConsentEntries.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { customStateOf, stashCustomState } from '../plugin/customState'\nimport type { ConsentSourceEntry, ConsentSourcesResolver } from './types'\n\ntype ConsentCustomState = { consentSources: ConsentSourcesResolver }\n\n/** Park the host's resolver on the config so root-level helpers can find it via `payload.config`. */\nexport const stashConsentSources = (\n\tcustom: Record<string, unknown> | undefined,\n\tsources: ConsentSourcesResolver\n): Record<string, unknown> =>\n\tstashCustomState<ConsentCustomState>(custom, { consentSources: sources })\n\nexport const consentSourcesOf = (payload: Payload): ConsentSourcesResolver | undefined =>\n\tcustomStateOf<ConsentCustomState>(payload).consentSources\n\nconst CACHE_KEY = 'formBuilderConsentSourcesCache'\n\ntype EntriesCache = Map<string, ConsentSourceEntry[]>\n\nconst cacheOf = (req: PayloadRequest): EntriesCache | undefined => {\n\tif (!req.context) {\n\t\treturn undefined\n\t}\n\tconst existing = req.context[CACHE_KEY]\n\tif (existing instanceof Map) {\n\t\treturn existing as EntriesCache\n\t}\n\tconst created: EntriesCache = new Map()\n\treq.context[CACHE_KEY] = created\n\treturn created\n}\n\nexport type ResolveConsentEntriesArgs = {\n\tpayload: Payload\n\treq: PayloadRequest\n\t/** The form
|
|
1
|
+
{"version":3,"file":"resolveConsentEntries.js","names":[],"sources":["../../src/consent/resolveConsentEntries.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { customStateOf, stashCustomState } from '../plugin/customState'\nimport type { ConsentSourceEntry, ConsentSourcesResolver } from './types'\n\ntype ConsentCustomState = { consentSources: ConsentSourcesResolver }\n\n/** Park the host's resolver on the config so root-level helpers can find it via `payload.config`. */\nexport const stashConsentSources = (\n\tcustom: Record<string, unknown> | undefined,\n\tsources: ConsentSourcesResolver\n): Record<string, unknown> =>\n\tstashCustomState<ConsentCustomState>(custom, { consentSources: sources })\n\nexport const consentSourcesOf = (payload: Payload): ConsentSourcesResolver | undefined =>\n\tcustomStateOf<ConsentCustomState>(payload).consentSources\n\nconst CACHE_KEY = 'formBuilderConsentSourcesCache'\n\ntype EntriesCache = Map<string, ConsentSourceEntry[]>\n\nconst cacheOf = (req: PayloadRequest): EntriesCache | undefined => {\n\tif (!req.context) {\n\t\treturn undefined\n\t}\n\tconst existing = req.context[CACHE_KEY]\n\tif (existing instanceof Map) {\n\t\treturn existing as EntriesCache\n\t}\n\tconst created: EntriesCache = new Map()\n\treq.context[CACHE_KEY] = created\n\treturn created\n}\n\nexport type ResolveConsentEntriesArgs = {\n\tpayload: Payload\n\treq: PayloadRequest\n\t/** The whole form being resolved; forwarded to the resolver, which casts for non-standard fields (e.g. tenant). */\n\tform: { id: number | string; title?: string | null }\n\t/** Resolver override for plugin-internal callers; defaults to the one the plugin stashed on the config. */\n\tsources?: ConsentSourcesResolver\n}\n\n/**\n * The host's consent sources for this request, or an empty list when no resolver is configured\n * (in which case the `consent` field type is not registered either, so nothing can reference one).\n * Throws whatever the resolver throws: every caller decides how to fail, and none of them fail open.\n * Cached per request and form id, so one submission resolves the host's sources once no matter how\n * many consent fields the form carries.\n */\nexport const resolveConsentEntries = async (\n\targs: ResolveConsentEntriesArgs\n): Promise<ConsentSourceEntry[]> => {\n\tconst { payload, req, form } = args\n\tconst resolver = args.sources ?? consentSourcesOf(payload)\n\tif (!resolver) {\n\t\treturn []\n\t}\n\tconst cache = cacheOf(req)\n\tconst cacheKey = String(form.id)\n\tconst cached = cache?.get(cacheKey)\n\tif (cached) {\n\t\treturn cached\n\t}\n\tconst entries = await resolver({ req, form })\n\tcache?.set(cacheKey, entries)\n\treturn entries\n}\n"],"mappings":";;;AAOA,MAAa,uBACZ,QACA,YAEA,iBAAqC,QAAQ,EAAE,gBAAgB,QAAQ,CAAC;AAEzE,MAAa,oBAAoB,YAChC,cAAkC,OAAO,EAAE;AAE5C,MAAM,YAAY;AAIlB,MAAM,WAAW,QAAkD;CAClE,IAAI,CAAC,IAAI,SACR;CAED,MAAM,WAAW,IAAI,QAAQ;CAC7B,IAAI,oBAAoB,KACvB,OAAO;CAER,MAAM,0BAAwB,IAAI,IAAI;CACtC,IAAI,QAAQ,aAAa;CACzB,OAAO;AACR;;;;;;;;AAkBA,MAAa,wBAAwB,OACpC,SACmC;CACnC,MAAM,EAAE,SAAS,KAAK,SAAS;CAC/B,MAAM,WAAW,KAAK,WAAW,iBAAiB,OAAO;CACzD,IAAI,CAAC,UACJ,OAAO,CAAC;CAET,MAAM,QAAQ,QAAQ,GAAG;CACzB,MAAM,WAAW,OAAO,KAAK,EAAE;CAC/B,MAAM,SAAS,OAAO,IAAI,QAAQ;CAClC,IAAI,QACH,OAAO;CAER,MAAM,UAAU,MAAM,SAAS;EAAE;EAAK;CAAK,CAAC;CAC5C,OAAO,IAAI,UAAU,OAAO;CAC5B,OAAO;AACR"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolveConsentSourcesRequest.js","names":[],"sources":["../../src/consent/resolveConsentSourcesRequest.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { FORMS_SLUG } from '../collections/forms'\nimport { resolveConsentEntries } from './resolveConsentEntries'\nimport type { ConsentSourcesResolver } from './types'\n\n/** One selectable consent source for the consent field's `source` select. */\nexport type ConsentSourceOption = { label: string; value: string }\n\nexport type ResolveConsentSourcesRequestArgs = {\n\tpayload: Payload\n\tformId: number | string | undefined\n\t/** Whether the caller is authenticated (an admin/user). */\n\tisAuthed: boolean\n\treq: PayloadRequest\n\tresolver: ConsentSourcesResolver\n}\n\nexport type ResolveConsentSourcesRequestResult = {\n\tstatus: number\n\tbody: { options: ConsentSourceOption[] } | { errors: { message: string }[] }\n}\n\n/**\n * Authorize and resolve the `GET /:id/consent-sources` request backing the consent `source` select:\n * authenticated callers get the host's sources for this request (a multi-tenant resolver scoping by\n * `req` therefore offers an author only their own tenant's), anonymous callers are always refused.\n * Statuses mirror the poll-options endpoint: 400 missing id, 403 unauthenticated, 404 unknown form,\n * 503 when the resolver throws (fail closed).\n */\nexport const resolveConsentSourcesRequest = async (\n\targs: ResolveConsentSourcesRequestArgs\n): Promise<ResolveConsentSourcesRequestResult> => {\n\tconst { payload, formId, isAuthed, req, resolver } = args\n\tif (!isAuthed) {\n\t\treturn { status: 403, body: { errors: [{ message: 'Forbidden' }] } }\n\t}\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,
|
|
1
|
+
{"version":3,"file":"resolveConsentSourcesRequest.js","names":[],"sources":["../../src/consent/resolveConsentSourcesRequest.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { FORMS_SLUG } from '../collections/forms'\nimport { resolveConsentEntries } from './resolveConsentEntries'\nimport type { ConsentSourcesResolver } from './types'\n\n/** One selectable consent source for the consent field's `source` select. */\nexport type ConsentSourceOption = { label: string; value: string }\n\nexport type ResolveConsentSourcesRequestArgs = {\n\tpayload: Payload\n\tformId: number | string | undefined\n\t/** Whether the caller is authenticated (an admin/user). */\n\tisAuthed: boolean\n\treq: PayloadRequest\n\tresolver: ConsentSourcesResolver\n}\n\nexport type ResolveConsentSourcesRequestResult = {\n\tstatus: number\n\tbody: { options: ConsentSourceOption[] } | { errors: { message: string }[] }\n}\n\n/**\n * Authorize and resolve the `GET /:id/consent-sources` request backing the consent `source` select:\n * authenticated callers get the host's sources for this request (a multi-tenant resolver scoping by\n * `req` therefore offers an author only their own tenant's), anonymous callers are always refused.\n * Statuses mirror the poll-options endpoint: 400 missing id, 403 unauthenticated, 404 unknown form,\n * 503 when the resolver throws (fail closed).\n */\nexport const resolveConsentSourcesRequest = async (\n\targs: ResolveConsentSourcesRequestArgs\n): Promise<ResolveConsentSourcesRequestResult> => {\n\tconst { payload, formId, isAuthed, req, resolver } = args\n\tif (!isAuthed) {\n\t\treturn { status: 403, body: { errors: [{ message: 'Forbidden' }] } }\n\t}\n\tif (formId == null) {\n\t\treturn { status: 400, body: { errors: [{ message: 'Missing form id' }] } }\n\t}\n\t// Load under the caller's own read access (no overrideAccess): a tenant-scoped `forms.access.read`\n\t// then refuses a cross-tenant form id (null -> 404), so an authed user can never enumerate another\n\t// tenant's consent source labels/ids.\n\tconst form = await payload\n\t\t.findByID({ collection: FORMS_SLUG, id: formId, depth: 0, req })\n\t\t.catch(() => null)\n\tif (!form) {\n\t\treturn { status: 404, body: { errors: [{ message: 'Not found' }] } }\n\t}\n\ttry {\n\t\tconst entries = await resolveConsentEntries({ payload, req, form, sources: resolver })\n\t\treturn {\n\t\t\tstatus: 200,\n\t\t\tbody: {\n\t\t\t\toptions: entries.map((entry) => ({ label: entry.name ?? entry.id, value: entry.id })),\n\t\t\t},\n\t\t}\n\t} catch {\n\t\treturn { status: 503, body: { errors: [{ message: 'Consent sources unavailable' }] } }\n\t}\n}\n"],"mappings":";;;;;;;;;;AA6BA,MAAa,+BAA+B,OAC3C,SACiD;CACjD,MAAM,EAAE,SAAS,QAAQ,UAAU,KAAK,aAAa;CACrD,IAAI,CAAC,UACJ,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,EAAE;CAAE;CAEpE,IAAI,UAAU,MACb,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,kBAAkB,CAAC,EAAE;CAAE;CAK1E,MAAM,OAAO,MAAM,QACjB,SAAS;EAAE,YAAY;EAAY,IAAI;EAAQ,OAAO;EAAG;CAAI,CAAC,EAC9D,YAAY,IAAI;CAClB,IAAI,CAAC,MACJ,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,EAAE;CAAE;CAEpE,IAAI;EAEH,OAAO;GACN,QAAQ;GACR,MAAM,EACL,UAAS,MAJW,sBAAsB;IAAE;IAAS;IAAK;IAAM,SAAS;GAAS,CAAC,GAIlE,KAAK,WAAW;IAAE,OAAO,MAAM,QAAQ,MAAM;IAAI,OAAO,MAAM;GAAG,EAAE,EACrF;EACD;CACD,QAAQ;EACP,OAAO;GAAE,QAAQ;GAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,8BAA8B,CAAC,EAAE;EAAE;CACtF;AACD"}
|
|
@@ -14,7 +14,7 @@ type ConsentStatement = {
|
|
|
14
14
|
type ConsentStatements = Record<string, ConsentStatement>;
|
|
15
15
|
type ResolveConsentStatementsArgs = {
|
|
16
16
|
payload: Payload;
|
|
17
|
-
req: PayloadRequest; /** A loaded forms document; its consent fields' `source` ids drive resolution. */
|
|
17
|
+
req: PayloadRequest; /** A loaded forms document; its consent fields' `source` ids drive resolution. The whole doc is forwarded to the resolver. */
|
|
18
18
|
form: {
|
|
19
19
|
id: number | string;
|
|
20
20
|
title?: string | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolveConsentStatements.js","names":[],"sources":["../../src/consent/resolveConsentStatements.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { resolveConsentEntries } from './resolveConsentEntries'\nimport type { ConsentSourcesResolver } from './types'\n\n/** What a consent field shows the visitor, resolved from its source at request time. */\nexport type ConsentStatement = {\n\t/** Rich text state (or whatever the host's resolver returned), for the request's locale. */\n\tstatement?: unknown\n\t/** The policy link beside the statement, present only when the source carries both a `url` and a `label`. */\n\tlink?: { label: string; url: string }\n}\n\n/** Resolved statements keyed by consent field name, as `toFormDocument`'s `consentStatements` takes them. */\nexport type ConsentStatements = Record<string, ConsentStatement>\n\nexport type ResolveConsentStatementsArgs = {\n\tpayload: Payload\n\treq: PayloadRequest\n\t/** A loaded forms document; its consent fields' `source` ids drive resolution. */\n\tform: {\n\t\tid: number | string\n\t\ttitle?: string | null\n\t\tfields?: { blockType: string; name?: string; [key: string]: unknown }[] | null\n\t}\n\t/** Resolver override for plugin-internal callers; defaults to the one the plugin stashed on the config. */\n\tsources?: ConsentSourcesResolver\n}\n\n/**\n * Resolve every consent field's statement for this request, ready to hand to\n * `toFormDocument(doc, { consentStatements })`.\n *\n * The statement is a live reference, not a copy: a form stores only its source id, and the\n * sentence the visitor reads is resolved here, in their locale, from whatever the source says\n * right now. So correcting a policy sentence corrects every form referencing it, with nothing to\n * re-save, and a form rendered without this step shows no statement at all rather than a stale one.\n *\n * Call it server-side (a Server Component, a route handler) before rendering `<Form>`. Throws\n * whatever the host's resolver throws, so a page can decide between failing and rendering without\n * consent; a submit resolves the source again regardless, and the proof is built from that.\n */\nexport const resolveConsentStatements = async (\n\targs: ResolveConsentStatementsArgs\n): Promise<ConsentStatements> => {\n\tconst { payload, req, form, sources } = args\n\tconst consentFields = (form.fields ?? []).filter(\n\t\t(field): field is { blockType: string; name: string; source?: unknown } =>\n\t\t\tfield.blockType === 'consent' && typeof field.name === 'string' && field.name.length > 0\n\t)\n\tif (consentFields.length === 0) {\n\t\treturn {}\n\t}\n\tconst entries = await resolveConsentEntries({ payload, req, form, sources })\n\tconst statements: ConsentStatements = {}\n\tfor (const field of consentFields) {\n\t\tconst entry = entries.find((candidate) => candidate.id === field.source)\n\t\tif (!entry) {\n\t\t\tcontinue\n\t\t}\n\t\t// The link needs a name of its own, so it takes the source's `name` and never falls back to\n\t\t// the `id`: the id is a machine identifier, and showing it as the words a visitor clicks is\n\t\t// worse than no link at all. A source with a url and no name simply has no link.\n\t\tconst label = typeof entry.name === 'string' && entry.name.trim() !== '' ? entry.name : ''\n\t\tstatements[field.name] = {\n\t\t\t...(entry.statement != null ? { statement: entry.statement } : {}),\n\t\t\t...(label && typeof entry.url === 'string' && entry.url.length > 0\n\t\t\t\t? { link: { label, url: entry.url } }\n\t\t\t\t: {}),\n\t\t}\n\t}\n\treturn statements\n}\n"],"mappings":";;;;;;;;;;;;;;;AAyCA,MAAa,2BAA2B,OACvC,SACgC;CAChC,MAAM,EAAE,SAAS,KAAK,MAAM,YAAY;CACxC,MAAM,iBAAiB,KAAK,UAAU,CAAC,GAAG,QACxC,UACA,MAAM,cAAc,aAAa,OAAO,MAAM,SAAS,YAAY,MAAM,KAAK,SAAS,CACzF;CACA,IAAI,cAAc,WAAW,GAC5B,OAAO,CAAC;CAET,MAAM,UAAU,MAAM,sBAAsB;EAAE;EAAS;EAAK;EAAM;CAAQ,CAAC;CAC3E,MAAM,aAAgC,CAAC;CACvC,KAAK,MAAM,SAAS,eAAe;EAClC,MAAM,QAAQ,QAAQ,MAAM,cAAc,UAAU,OAAO,MAAM,MAAM;EACvE,IAAI,CAAC,OACJ;EAKD,MAAM,QAAQ,OAAO,MAAM,SAAS,YAAY,MAAM,KAAK,KAAK,MAAM,KAAK,MAAM,OAAO;EACxF,WAAW,MAAM,QAAQ;GACxB,GAAI,MAAM,aAAa,OAAO,EAAE,WAAW,MAAM,UAAU,IAAI,CAAC;GAChE,GAAI,SAAS,OAAO,MAAM,QAAQ,YAAY,MAAM,IAAI,SAAS,IAC9D,EAAE,MAAM;IAAE;IAAO,KAAK,MAAM;GAAI,EAAE,IAClC,CAAC;EACL;CACD;CACA,OAAO;AACR"}
|
|
1
|
+
{"version":3,"file":"resolveConsentStatements.js","names":[],"sources":["../../src/consent/resolveConsentStatements.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { resolveConsentEntries } from './resolveConsentEntries'\nimport type { ConsentSourcesResolver } from './types'\n\n/** What a consent field shows the visitor, resolved from its source at request time. */\nexport type ConsentStatement = {\n\t/** Rich text state (or whatever the host's resolver returned), for the request's locale. */\n\tstatement?: unknown\n\t/** The policy link beside the statement, present only when the source carries both a `url` and a `label`. */\n\tlink?: { label: string; url: string }\n}\n\n/** Resolved statements keyed by consent field name, as `toFormDocument`'s `consentStatements` takes them. */\nexport type ConsentStatements = Record<string, ConsentStatement>\n\nexport type ResolveConsentStatementsArgs = {\n\tpayload: Payload\n\treq: PayloadRequest\n\t/** A loaded forms document; its consent fields' `source` ids drive resolution. The whole doc is forwarded to the resolver. */\n\tform: {\n\t\tid: number | string\n\t\ttitle?: string | null\n\t\tfields?: { blockType: string; name?: string; [key: string]: unknown }[] | null\n\t}\n\t/** Resolver override for plugin-internal callers; defaults to the one the plugin stashed on the config. */\n\tsources?: ConsentSourcesResolver\n}\n\n/**\n * Resolve every consent field's statement for this request, ready to hand to\n * `toFormDocument(doc, { consentStatements })`.\n *\n * The statement is a live reference, not a copy: a form stores only its source id, and the\n * sentence the visitor reads is resolved here, in their locale, from whatever the source says\n * right now. So correcting a policy sentence corrects every form referencing it, with nothing to\n * re-save, and a form rendered without this step shows no statement at all rather than a stale one.\n *\n * Call it server-side (a Server Component, a route handler) before rendering `<Form>`. Throws\n * whatever the host's resolver throws, so a page can decide between failing and rendering without\n * consent; a submit resolves the source again regardless, and the proof is built from that.\n */\nexport const resolveConsentStatements = async (\n\targs: ResolveConsentStatementsArgs\n): Promise<ConsentStatements> => {\n\tconst { payload, req, form, sources } = args\n\tconst consentFields = (form.fields ?? []).filter(\n\t\t(field): field is { blockType: string; name: string; source?: unknown } =>\n\t\t\tfield.blockType === 'consent' && typeof field.name === 'string' && field.name.length > 0\n\t)\n\tif (consentFields.length === 0) {\n\t\treturn {}\n\t}\n\tconst entries = await resolveConsentEntries({ payload, req, form, sources })\n\tconst statements: ConsentStatements = {}\n\tfor (const field of consentFields) {\n\t\tconst entry = entries.find((candidate) => candidate.id === field.source)\n\t\tif (!entry) {\n\t\t\tcontinue\n\t\t}\n\t\t// The link needs a name of its own, so it takes the source's `name` and never falls back to\n\t\t// the `id`: the id is a machine identifier, and showing it as the words a visitor clicks is\n\t\t// worse than no link at all. A source with a url and no name simply has no link.\n\t\tconst label = typeof entry.name === 'string' && entry.name.trim() !== '' ? entry.name : ''\n\t\tstatements[field.name] = {\n\t\t\t...(entry.statement != null ? { statement: entry.statement } : {}),\n\t\t\t...(label && typeof entry.url === 'string' && entry.url.length > 0\n\t\t\t\t? { link: { label, url: entry.url } }\n\t\t\t\t: {}),\n\t\t}\n\t}\n\treturn statements\n}\n"],"mappings":";;;;;;;;;;;;;;;AAyCA,MAAa,2BAA2B,OACvC,SACgC;CAChC,MAAM,EAAE,SAAS,KAAK,MAAM,YAAY;CACxC,MAAM,iBAAiB,KAAK,UAAU,CAAC,GAAG,QACxC,UACA,MAAM,cAAc,aAAa,OAAO,MAAM,SAAS,YAAY,MAAM,KAAK,SAAS,CACzF;CACA,IAAI,cAAc,WAAW,GAC5B,OAAO,CAAC;CAET,MAAM,UAAU,MAAM,sBAAsB;EAAE;EAAS;EAAK;EAAM;CAAQ,CAAC;CAC3E,MAAM,aAAgC,CAAC;CACvC,KAAK,MAAM,SAAS,eAAe;EAClC,MAAM,QAAQ,QAAQ,MAAM,cAAc,UAAU,OAAO,MAAM,MAAM;EACvE,IAAI,CAAC,OACJ;EAKD,MAAM,QAAQ,OAAO,MAAM,SAAS,YAAY,MAAM,KAAK,KAAK,MAAM,KAAK,MAAM,OAAO;EACxF,WAAW,MAAM,QAAQ;GACxB,GAAI,MAAM,aAAa,OAAO,EAAE,WAAW,MAAM,UAAU,IAAI,CAAC;GAChE,GAAI,SAAS,OAAO,MAAM,QAAQ,YAAY,MAAM,IAAI,SAAS,IAC9D,EAAE,MAAM;IAAE;IAAO,KAAK,MAAM;GAAI,EAAE,IAClC,CAAC;EACL;CACD;CACA,OAAO;AACR"}
|
package/dist/consent/types.d.ts
CHANGED
|
@@ -36,12 +36,24 @@ type ConsentSourceEntry = {
|
|
|
36
36
|
* cookie, or auth context) and return only that tenant's sources, so one tenant's statements are
|
|
37
37
|
* never selectable, resolvable, or provable under another's. Read under `req.locale` so the
|
|
38
38
|
* statement the visitor agrees to is the one they were shown.
|
|
39
|
+
*
|
|
40
|
+
* Do not thread the save's `req` into a `find({ locale: 'all' })` from within a form's save
|
|
41
|
+
* validation: it flips `req.locale` to `all` and corrupts the localized write of the document being
|
|
42
|
+
* saved. Read the settings document without the save's `req` (already-committed data, no transaction).
|
|
39
43
|
*/
|
|
40
44
|
type ConsentSourcesResolver = (args: {
|
|
41
45
|
req: PayloadRequest;
|
|
46
|
+
/**
|
|
47
|
+
* The form document being read: `id`, `title`, and (at runtime) every other field. Derive
|
|
48
|
+
* multi-tenant scoping from a field on it, casting for non-standard fields (e.g.
|
|
49
|
+
* `(form as { tenant?: string }).tenant`), rather than reading the form back: reading the same
|
|
50
|
+
* `forms` collection from within this resolver re-enters the form's afterRead hook. The type is
|
|
51
|
+
* kept index-signature-free so a generated `Form` interface assigns without a cast. Treat `form`
|
|
52
|
+
* as read-only.
|
|
53
|
+
*/
|
|
42
54
|
form: {
|
|
43
55
|
id: number | string;
|
|
44
|
-
title?: string;
|
|
56
|
+
title?: string | null;
|
|
45
57
|
};
|
|
46
58
|
}) => ConsentSourceEntry[] | Promise<ConsentSourceEntry[]>;
|
|
47
59
|
//#endregion
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import { Payload, PayloadRequest } from "payload";
|
|
2
2
|
|
|
3
3
|
//#region src/email/departments.d.ts
|
|
4
|
-
/** One selectable department for the `emailTeam`
|
|
4
|
+
/** One selectable department for the `emailTeam` recipients; `value` is the email address routed to. */
|
|
5
5
|
type DepartmentOption = {
|
|
6
6
|
label: string;
|
|
7
7
|
value: string;
|
|
8
8
|
};
|
|
9
9
|
/**
|
|
10
|
-
* Host seam resolving the selectable departments for `emailTeam`'s
|
|
10
|
+
* Host seam resolving the selectable departments for `emailTeam`'s recipient fields (plugin option
|
|
11
11
|
* `email.departments`). Req-scoped, so multi-tenant hosts derive tenant scoping from `req` (host
|
|
12
12
|
* header, cookie, or auth context) and return only that tenant's departments. `value` is the literal
|
|
13
|
-
* string `payload.sendEmail` accepts
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* string `payload.sendEmail` accepts (an email address); `label` is how the department is named in the
|
|
14
|
+
* admin select. Mirrors {@link FromAddressesResolver}. {@link resolveDepartmentOptions} builds this
|
|
15
|
+
* from a `departmentsField()`-bearing document with the reusable locale fallback, so a host does not
|
|
16
|
+
* reimplement it.
|
|
17
|
+
*
|
|
18
|
+
* Do not thread the save's `req` into a `find({ locale: 'all' })` from within a form's save
|
|
19
|
+
* validation: it flips `req.locale` to `all` and corrupts the localized write of the document being
|
|
20
|
+
* saved. Read the settings document without the save's `req` (already-committed data, no transaction).
|
|
17
21
|
*/
|
|
18
22
|
type DepartmentEmailsResolver = (args: {
|
|
19
23
|
req: PayloadRequest;
|
|
@@ -40,9 +44,9 @@ type ResolveDepartmentOptionsArgs = {
|
|
|
40
44
|
field?: string;
|
|
41
45
|
};
|
|
42
46
|
/**
|
|
43
|
-
* Build the `emailTeam`
|
|
44
|
-
* a tenants row, wherever the host placed it), fetched at `locale: 'all'`. Each row's `label`
|
|
45
|
-
* `email` are resolved for `req.locale` via the current -> default -> next-available chain (see
|
|
47
|
+
* Build the `emailTeam` recipient options from a `departmentsField()`-bearing document (a settings
|
|
48
|
+
* global, a tenants row, wherever the host placed it), fetched at `locale: 'all'`. Each row's `label`
|
|
49
|
+
* and `email` are resolved for `req.locale` via the current -> default -> next-available chain (see
|
|
46
50
|
* {@link pickLocaleValue}), reading `defaultLocale`/`localeCodes` from `payload.config.localization`
|
|
47
51
|
* (a bare string is used as-is when localization is disabled). Rows with no resolvable email are
|
|
48
52
|
* dropped; a row's label is resolved independently of its email and falls back to the email when
|