payload-dynamic-forms 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +461 -2
- package/dist/access.d.ts +7 -0
- package/dist/access.js +10 -0
- package/dist/access.js.map +1 -0
- package/dist/collections/attachments.d.ts +6 -0
- package/dist/collections/attachments.js +67 -0
- package/dist/collections/attachments.js.map +1 -0
- package/dist/collections/blocks.d.ts +27 -0
- package/dist/collections/blocks.js +495 -0
- package/dist/collections/blocks.js.map +1 -0
- package/dist/collections/forms.d.ts +3 -0
- package/dist/collections/forms.js +558 -0
- package/dist/collections/forms.js.map +1 -0
- package/dist/collections/overrides.d.ts +7 -0
- package/dist/collections/overrides.js +49 -0
- package/dist/collections/overrides.js.map +1 -0
- package/dist/collections/ruleFields.d.ts +6 -0
- package/dist/collections/ruleFields.js +97 -0
- package/dist/collections/ruleFields.js.map +1 -0
- package/dist/collections/submissions.d.ts +7 -0
- package/dist/collections/submissions.js +225 -0
- package/dist/collections/submissions.js.map +1 -0
- package/dist/delivery/run.d.ts +37 -0
- package/dist/delivery/run.js +234 -0
- package/dist/delivery/run.js.map +1 -0
- package/dist/delivery/webhook.d.ts +41 -0
- package/dist/delivery/webhook.js +120 -0
- package/dist/delivery/webhook.js.map +1 -0
- package/dist/endpoints/public.d.ts +4 -0
- package/dist/endpoints/public.js +27 -0
- package/dist/endpoints/public.js.map +1 -0
- package/dist/endpoints/resend.d.ts +4 -0
- package/dist/endpoints/resend.js +58 -0
- package/dist/endpoints/resend.js.map +1 -0
- package/dist/endpoints/submit.d.ts +4 -0
- package/dist/endpoints/submit.js +320 -0
- package/dist/endpoints/submit.js.map +1 -0
- package/dist/exports/shared.d.ts +13 -0
- package/dist/exports/shared.js +13 -0
- package/dist/exports/shared.js.map +1 -0
- package/dist/formBlock.d.ts +13 -0
- package/dist/formBlock.js +25 -0
- package/dist/formBlock.js.map +1 -0
- package/dist/hooks/resendTick.d.ts +8 -0
- package/dist/hooks/resendTick.js +55 -0
- package/dist/hooks/resendTick.js.map +1 -0
- package/dist/hooks/validateForm.d.ts +8 -0
- package/dist/hooks/validateForm.js +251 -0
- package/dist/hooks/validateForm.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +130 -0
- package/dist/index.js.map +1 -0
- package/dist/security/crypto.d.ts +9 -0
- package/dist/security/crypto.js +54 -0
- package/dist/security/crypto.js.map +1 -0
- package/dist/security/rateLimit.d.ts +13 -0
- package/dist/security/rateLimit.js +28 -0
- package/dist/security/rateLimit.js.map +1 -0
- package/dist/security/request.d.ts +3 -0
- package/dist/security/request.js +13 -0
- package/dist/security/request.js.map +1 -0
- package/dist/security/turnstile.d.ts +28 -0
- package/dist/security/turnstile.js +56 -0
- package/dist/security/turnstile.js.map +1 -0
- package/dist/server/forms.d.ts +73 -0
- package/dist/server/forms.js +215 -0
- package/dist/server/forms.js.map +1 -0
- package/dist/server/resend.d.ts +27 -0
- package/dist/server/resend.js +84 -0
- package/dist/server/resend.js.map +1 -0
- package/dist/shared/answers.d.ts +47 -0
- package/dist/shared/answers.js +282 -0
- package/dist/shared/answers.js.map +1 -0
- package/dist/shared/operators.d.ts +41 -0
- package/dist/shared/operators.js +54 -0
- package/dist/shared/operators.js.map +1 -0
- package/dist/shared/rules.d.ts +58 -0
- package/dist/shared/rules.js +324 -0
- package/dist/shared/rules.js.map +1 -0
- package/dist/shared/steps.d.ts +28 -0
- package/dist/shared/steps.js +109 -0
- package/dist/shared/steps.js.map +1 -0
- package/dist/shared/tokens.d.ts +25 -0
- package/dist/shared/tokens.js +96 -0
- package/dist/shared/tokens.js.map +1 -0
- package/dist/shared/types.d.ts +181 -0
- package/dist/shared/types.js +5 -0
- package/dist/shared/types.js.map +1 -0
- package/dist/shared/whatsapp.d.ts +20 -0
- package/dist/shared/whatsapp.js +26 -0
- package/dist/shared/whatsapp.js.map +1 -0
- package/dist/shared/words.d.ts +9 -0
- package/dist/shared/words.js +41 -0
- package/dist/shared/words.js.map +1 -0
- package/dist/types.d.ts +182 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +114 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/shared/tokens.ts"],"sourcesContent":["import type { Answers, FieldTypeChecks, FormLike, InputBlock } from './types.js'\n\nimport { answerToText } from './answers.js'\nimport { getInputFields, getOptions, getRowLabel, hasAnswer, isChoiceBlock, toStringList } from './rules.js'\nimport { getVisibility } from './steps.js'\nimport { fillWords } from './words.js'\n\nexport type TokenValue = boolean | null | number | string | undefined\n\nexport type MessageRow = { key: string; label: string; value: string }\n\nexport type InterpolateOptions = {\n /** Escape values for HTML and turn `{{*}}` lines into `<br>`. */\n html?: boolean\n /** Rows for `{{*}}` and `{{*:table}}`. */\n rows?: MessageRow[]\n /** Remove line breaks (for subjects and headers). */\n singleLine?: boolean\n}\n\nconst TOKEN = /\\{\\{\\s*(\\*(?::table)?|\\w[\\w-]*(?:\\.[\\w-]+)?)\\s*\\}\\}/g\n\nexport function escapeHtml(value: string): string {\n return value\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n}\n\nfunction rowsAsLines(rows: MessageRow[], html: boolean): string {\n const lines = rows.map((row) => `${row.label}: ${row.value}`)\n return html ? lines.map(escapeHtml).join('<br>\\n') : lines.join('\\n')\n}\n\nfunction rowsAsTable(rows: MessageRow[], html: boolean): string {\n if (!html) {\n return rowsAsLines(rows, false)\n }\n const body = rows\n .map((row) => `<tr><th align=\"left\">${escapeHtml(row.label)}</th><td>${escapeHtml(row.value)}</td></tr>`)\n .join('')\n return `<table><tbody>${body}</tbody></table>`\n}\n\n/**\n * Fills `{{key}}` (dashes and underscores allowed), `{{key.attribute}}`, `{{*}}` and\n * `{{*:table}}`. Unknown tokens become empty.\n */\nexport function interpolate(\n template: null | string | undefined,\n values: Record<string, TokenValue>,\n options: InterpolateOptions = {},\n): string {\n if (!template) {\n return ''\n }\n const html = Boolean(options.html)\n const out = template.replace(TOKEN, (_match, name: string) => {\n if (name === '*') {\n return rowsAsLines(options.rows || [], html)\n }\n if (name === '*:table') {\n return rowsAsTable(options.rows || [], html)\n }\n const value = values[name]\n const text = value === undefined || value === null ? '' : String(value)\n return html ? escapeHtml(text) : text\n })\n return options.singleLine ? out.replace(/[\\r\\n]+/g, ' ').trim() : out\n}\n\n/** One row per answered, shown field, in form order, using each field's row label. */\nexport function buildMessageRows(\n form: FormLike,\n answers: Answers,\n fieldTypes?: Record<string, FieldTypeChecks>,\n): MessageRow[] {\n const { answers: shown } = getVisibility(form, answers)\n const words = fillWords(form.wording)\n const rows: MessageRow[] = []\n for (const { field, key } of getInputFields(form)) {\n if (!(key in shown) || !hasAnswer(shown[key])) {\n continue\n }\n const value = answerToText(field, shown[key], words, fieldTypes)\n if (!value.trim()) {\n continue\n }\n rows.push({ key, label: getRowLabel(field, key), value })\n }\n return rows\n}\n\nfunction chosenAttributes(field: InputBlock, value: unknown): Record<string, string> {\n const options = getOptions(field)\n const chosen = toStringList(value)\n .map((v) => options.find((o) => String(o.value) === v))\n .filter(Boolean)\n const out: Record<string, string> = {}\n for (const option of chosen) {\n for (const [name, attr] of Object.entries(option!.attributes || {})) {\n if (attr === null || attr === undefined) {\n continue\n }\n out[name] = out[name] ? `${out[name]}, ${String(attr)}` : String(attr)\n }\n }\n return out\n}\n\n/** Token values for a form's shown answers: `{{key}}`, `{{key.attribute}}` plus `extra`. */\nexport function buildTemplateValues(\n form: FormLike,\n answers: Answers,\n extra: Record<string, TokenValue> = {},\n fieldTypes?: Record<string, FieldTypeChecks>,\n): Record<string, TokenValue> {\n const { answers: shown } = getVisibility(form, answers)\n const words = fillWords(form.wording)\n const values: Record<string, TokenValue> = { formTitle: form.title || '', ...extra }\n for (const { field, key } of getInputFields(form)) {\n if (!(key in shown)) {\n continue\n }\n values[key] = answerToText(field, shown[key], words, fieldTypes)\n if (isChoiceBlock(field)) {\n for (const [name, attr] of Object.entries(chosenAttributes(field, shown[key]))) {\n values[`${key}.${name}`] = attr\n }\n }\n }\n return values\n}\n"],"names":["answerToText","getInputFields","getOptions","getRowLabel","hasAnswer","isChoiceBlock","toStringList","getVisibility","fillWords","TOKEN","escapeHtml","value","replace","rowsAsLines","rows","html","lines","map","row","label","join","rowsAsTable","body","interpolate","template","values","options","Boolean","out","_match","name","text","undefined","String","singleLine","trim","buildMessageRows","form","answers","fieldTypes","shown","words","wording","field","key","push","chosenAttributes","chosen","v","find","o","filter","option","attr","Object","entries","attributes","buildTemplateValues","extra","formTitle","title"],"mappings":"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"}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types shared by the browser and the server. Type-only: nothing here runs.
|
|
3
|
+
*/
|
|
4
|
+
import type { OperatorValue } from './operators.js';
|
|
5
|
+
export type MatchMode = 'all' | 'any';
|
|
6
|
+
export type PresentationMode = 'singlePage' | 'steps';
|
|
7
|
+
export type IntakeState = 'closed' | 'open';
|
|
8
|
+
export type SubmissionStatus = 'failed' | 'partlyFailed' | 'processed' | 'received';
|
|
9
|
+
export type RuleRow = {
|
|
10
|
+
/** A field key, or `<field key>.<attribute>` for an attribute of the chosen option. */
|
|
11
|
+
fieldKey?: null | string;
|
|
12
|
+
id?: null | string;
|
|
13
|
+
operator?: null | OperatorValue;
|
|
14
|
+
value?: null | string;
|
|
15
|
+
};
|
|
16
|
+
export type RuleGroup = {
|
|
17
|
+
match?: MatchMode | null;
|
|
18
|
+
rows?: null | RuleRow[];
|
|
19
|
+
};
|
|
20
|
+
export type NextStepRule = {
|
|
21
|
+
targetSectionKey?: null | string;
|
|
22
|
+
} & RuleGroup;
|
|
23
|
+
/** A choice as the renderer and the checks see it (typed or resolved from a source). */
|
|
24
|
+
export type ChoiceOption = {
|
|
25
|
+
attributes?: Record<string, unknown>;
|
|
26
|
+
label?: null | string;
|
|
27
|
+
value?: null | string;
|
|
28
|
+
};
|
|
29
|
+
/** The words one question shows. */
|
|
30
|
+
export type FieldWords = {
|
|
31
|
+
errorEmpty?: null | string;
|
|
32
|
+
errorInvalid?: null | string;
|
|
33
|
+
hint?: null | string;
|
|
34
|
+
label?: null | string;
|
|
35
|
+
placeholder?: null | string;
|
|
36
|
+
/** Label of this answer's row in messages; falls back to `label`. */
|
|
37
|
+
rowLabel?: null | string;
|
|
38
|
+
};
|
|
39
|
+
export type BuiltInFieldType = 'checkbox' | 'checkboxGroup' | 'date' | 'email' | 'fileUpload' | 'heading' | 'helpText' | 'number' | 'radio' | 'select' | 'text' | 'textarea';
|
|
40
|
+
export type DisplayBlock = {
|
|
41
|
+
blockName?: null | string;
|
|
42
|
+
blockType: 'heading' | 'helpText';
|
|
43
|
+
body?: null | string;
|
|
44
|
+
heading?: null | string;
|
|
45
|
+
id?: null | string;
|
|
46
|
+
level?: 'h2' | 'h3' | 'h4' | null;
|
|
47
|
+
};
|
|
48
|
+
export type InputBlock = {
|
|
49
|
+
[extra: string]: unknown;
|
|
50
|
+
blockName?: null | string;
|
|
51
|
+
/** Any input type: a built-in one or one an app registered. */
|
|
52
|
+
blockType: string;
|
|
53
|
+
defaultValue?: unknown;
|
|
54
|
+
fieldKey?: null | string;
|
|
55
|
+
id?: null | string;
|
|
56
|
+
max?: null | number;
|
|
57
|
+
maxDate?: null | string;
|
|
58
|
+
/** Files per field (file upload). */
|
|
59
|
+
maxFiles?: null | number;
|
|
60
|
+
/** Largest file in MB (file upload). */
|
|
61
|
+
maxFileSizeMB?: null | number;
|
|
62
|
+
maxLength?: null | number;
|
|
63
|
+
/** Allowed MIME types (file upload); empty = the plugin's list. */
|
|
64
|
+
mimeTypes?: null | string[];
|
|
65
|
+
min?: null | number;
|
|
66
|
+
minDate?: null | string;
|
|
67
|
+
minLength?: null | number;
|
|
68
|
+
options?: ChoiceOption[] | null;
|
|
69
|
+
optionsFrom?: null | string;
|
|
70
|
+
/** Name of a link (search) parameter that prefills this input. */
|
|
71
|
+
prefillParam?: null | string;
|
|
72
|
+
required?: boolean | null;
|
|
73
|
+
visibilityRules?: null | RuleGroup;
|
|
74
|
+
width?: 'full' | 'half' | null;
|
|
75
|
+
words?: FieldWords | null;
|
|
76
|
+
};
|
|
77
|
+
export type FieldBlock = DisplayBlock | InputBlock;
|
|
78
|
+
export type FormSection = {
|
|
79
|
+
description?: null | string;
|
|
80
|
+
fields?: FieldBlock[] | null;
|
|
81
|
+
id?: null | string;
|
|
82
|
+
key?: null | string;
|
|
83
|
+
nextStepRules?: NextStepRule[] | null;
|
|
84
|
+
title?: null | string;
|
|
85
|
+
visibilityRules?: null | RuleGroup;
|
|
86
|
+
};
|
|
87
|
+
export type FormWords = {
|
|
88
|
+
back: string;
|
|
89
|
+
chooseOne: string;
|
|
90
|
+
closed: string;
|
|
91
|
+
continue: string;
|
|
92
|
+
errorPrefix: string;
|
|
93
|
+
fileRejected: string;
|
|
94
|
+
genericError: string;
|
|
95
|
+
invalid: string;
|
|
96
|
+
/** An unticked checkbox in messages, tokens and the snapshot. */
|
|
97
|
+
no: string;
|
|
98
|
+
notFound: string;
|
|
99
|
+
optionalHint: string;
|
|
100
|
+
required: string;
|
|
101
|
+
securityCheckFailed: string;
|
|
102
|
+
send: string;
|
|
103
|
+
sending: string;
|
|
104
|
+
sent: string;
|
|
105
|
+
stepProgress: string;
|
|
106
|
+
success: string;
|
|
107
|
+
tooManyTries: string;
|
|
108
|
+
/** A ticked checkbox in messages, tokens and the snapshot. */
|
|
109
|
+
yes: string;
|
|
110
|
+
};
|
|
111
|
+
/** The words `answerToText` needs: the form's `yes` and `no`. */
|
|
112
|
+
export type YesNoWords = Pick<FormWords, 'no' | 'yes'>;
|
|
113
|
+
/** The shape the rule engine needs: sections (with resolved choices) and optionally the words. */
|
|
114
|
+
export type FormLike = {
|
|
115
|
+
presentationMode?: null | PresentationMode;
|
|
116
|
+
sections?: FormSection[] | null;
|
|
117
|
+
title?: null | string;
|
|
118
|
+
wording?: null | Partial<FormWords>;
|
|
119
|
+
};
|
|
120
|
+
/** A form as `getPublicForm` and `GET /api/<forms>/public/:slug` return it. */
|
|
121
|
+
export type PublicForm = {
|
|
122
|
+
honeypotField: string;
|
|
123
|
+
id: number | string;
|
|
124
|
+
intakeState: IntakeState;
|
|
125
|
+
presentationMode: PresentationMode;
|
|
126
|
+
sections: FormSection[];
|
|
127
|
+
slug: string;
|
|
128
|
+
/** True when the server runs the honeypot, Turnstile and rate limit for this form. */
|
|
129
|
+
spamChecks: boolean;
|
|
130
|
+
successRedirectURL?: null | string;
|
|
131
|
+
successType: 'message' | 'redirect';
|
|
132
|
+
title: string;
|
|
133
|
+
turnstile: {
|
|
134
|
+
enabled: boolean;
|
|
135
|
+
fieldName: string;
|
|
136
|
+
};
|
|
137
|
+
whatsapp?: {
|
|
138
|
+
number: string;
|
|
139
|
+
};
|
|
140
|
+
wording: FormWords;
|
|
141
|
+
};
|
|
142
|
+
export type FileAnswer = {
|
|
143
|
+
attachmentIds: Array<number | string>;
|
|
144
|
+
filenames: string[];
|
|
145
|
+
};
|
|
146
|
+
export type AnswerValue = boolean | FileAnswer | null | number | string | string[];
|
|
147
|
+
export type Answers = Record<string, AnswerValue>;
|
|
148
|
+
/** A file as the checks see it: a browser `File` or anything with name, type and size. */
|
|
149
|
+
export type FileLike = {
|
|
150
|
+
name?: string;
|
|
151
|
+
size: number;
|
|
152
|
+
type?: string;
|
|
153
|
+
};
|
|
154
|
+
export type SnapshotRow = {
|
|
155
|
+
fieldKey: string;
|
|
156
|
+
label: string;
|
|
157
|
+
sectionKey: string;
|
|
158
|
+
sectionTitle: string;
|
|
159
|
+
value: string;
|
|
160
|
+
};
|
|
161
|
+
/** Checks an app adds for its own field type (the plugin's `fields` registry). */
|
|
162
|
+
export type FieldTypeChecks = {
|
|
163
|
+
normalize?: (raw: unknown, field: InputBlock) => AnswerValue;
|
|
164
|
+
toText?: (value: AnswerValue, field: InputBlock, words: YesNoWords) => string;
|
|
165
|
+
/** Return `true` when the value is fine, or the error words to show. */
|
|
166
|
+
validate?: (value: AnswerValue, ctx: {
|
|
167
|
+
answers: Answers;
|
|
168
|
+
field: InputBlock;
|
|
169
|
+
}) => string | true;
|
|
170
|
+
};
|
|
171
|
+
export type SubmitResult = {
|
|
172
|
+
fieldErrors?: Record<string, string>;
|
|
173
|
+
message: string;
|
|
174
|
+
ok: false;
|
|
175
|
+
} | {
|
|
176
|
+
message: string;
|
|
177
|
+
ok: true;
|
|
178
|
+
redirectURL?: string;
|
|
179
|
+
submissionId?: number | string;
|
|
180
|
+
whatsappUrl?: string;
|
|
181
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/shared/types.ts"],"sourcesContent":["/**\n * Types shared by the browser and the server. Type-only: nothing here runs.\n */\n\nimport type { OperatorValue } from './operators.js'\n\nexport type MatchMode = 'all' | 'any'\nexport type PresentationMode = 'singlePage' | 'steps'\nexport type IntakeState = 'closed' | 'open'\nexport type SubmissionStatus = 'failed' | 'partlyFailed' | 'processed' | 'received'\n\nexport type RuleRow = {\n /** A field key, or `<field key>.<attribute>` for an attribute of the chosen option. */\n fieldKey?: null | string\n id?: null | string\n operator?: null | OperatorValue\n value?: null | string\n}\n\nexport type RuleGroup = {\n match?: MatchMode | null\n rows?: null | RuleRow[]\n}\n\nexport type NextStepRule = {\n targetSectionKey?: null | string\n} & RuleGroup\n\n/** A choice as the renderer and the checks see it (typed or resolved from a source). */\nexport type ChoiceOption = {\n attributes?: Record<string, unknown>\n label?: null | string\n value?: null | string\n}\n\n/** The words one question shows. */\nexport type FieldWords = {\n errorEmpty?: null | string\n errorInvalid?: null | string\n hint?: null | string\n label?: null | string\n placeholder?: null | string\n /** Label of this answer's row in messages; falls back to `label`. */\n rowLabel?: null | string\n}\n\nexport type BuiltInFieldType =\n | 'checkbox'\n | 'checkboxGroup'\n | 'date'\n | 'email'\n | 'fileUpload'\n | 'heading'\n | 'helpText'\n | 'number'\n | 'radio'\n | 'select'\n | 'text'\n | 'textarea'\n\nexport type DisplayBlock = {\n blockName?: null | string\n blockType: 'heading' | 'helpText'\n body?: null | string\n heading?: null | string\n id?: null | string\n level?: 'h2' | 'h3' | 'h4' | null\n}\n\nexport type InputBlock = {\n [extra: string]: unknown\n blockName?: null | string\n /** Any input type: a built-in one or one an app registered. */\n blockType: string\n defaultValue?: unknown\n fieldKey?: null | string\n id?: null | string\n max?: null | number\n maxDate?: null | string\n /** Files per field (file upload). */\n maxFiles?: null | number\n /** Largest file in MB (file upload). */\n maxFileSizeMB?: null | number\n maxLength?: null | number\n /** Allowed MIME types (file upload); empty = the plugin's list. */\n mimeTypes?: null | string[]\n min?: null | number\n minDate?: null | string\n minLength?: null | number\n options?: ChoiceOption[] | null\n optionsFrom?: null | string\n /** Name of a link (search) parameter that prefills this input. */\n prefillParam?: null | string\n required?: boolean | null\n visibilityRules?: null | RuleGroup\n width?: 'full' | 'half' | null\n words?: FieldWords | null\n}\n\nexport type FieldBlock = DisplayBlock | InputBlock\n\nexport type FormSection = {\n description?: null | string\n fields?: FieldBlock[] | null\n id?: null | string\n key?: null | string\n nextStepRules?: NextStepRule[] | null\n title?: null | string\n visibilityRules?: null | RuleGroup\n}\n\nexport type FormWords = {\n back: string\n chooseOne: string\n closed: string\n continue: string\n errorPrefix: string\n fileRejected: string\n genericError: string\n invalid: string\n /** An unticked checkbox in messages, tokens and the snapshot. */\n no: string\n notFound: string\n optionalHint: string\n required: string\n securityCheckFailed: string\n send: string\n sending: string\n sent: string\n stepProgress: string\n success: string\n tooManyTries: string\n /** A ticked checkbox in messages, tokens and the snapshot. */\n yes: string\n}\n\n/** The words `answerToText` needs: the form's `yes` and `no`. */\nexport type YesNoWords = Pick<FormWords, 'no' | 'yes'>\n\n/** The shape the rule engine needs: sections (with resolved choices) and optionally the words. */\nexport type FormLike = {\n presentationMode?: null | PresentationMode\n sections?: FormSection[] | null\n title?: null | string\n wording?: null | Partial<FormWords>\n}\n\n/** A form as `getPublicForm` and `GET /api/<forms>/public/:slug` return it. */\nexport type PublicForm = {\n honeypotField: string\n id: number | string\n intakeState: IntakeState\n presentationMode: PresentationMode\n sections: FormSection[]\n slug: string\n /** True when the server runs the honeypot, Turnstile and rate limit for this form. */\n spamChecks: boolean\n successRedirectURL?: null | string\n successType: 'message' | 'redirect'\n title: string\n turnstile: { enabled: boolean; fieldName: string }\n whatsapp?: { number: string }\n wording: FormWords\n}\n\nexport type FileAnswer = {\n attachmentIds: Array<number | string>\n filenames: string[]\n}\n\nexport type AnswerValue = boolean | FileAnswer | null | number | string | string[]\n\nexport type Answers = Record<string, AnswerValue>\n\n/** A file as the checks see it: a browser `File` or anything with name, type and size. */\nexport type FileLike = { name?: string; size: number; type?: string }\n\nexport type SnapshotRow = {\n fieldKey: string\n label: string\n sectionKey: string\n sectionTitle: string\n value: string\n}\n\n/** Checks an app adds for its own field type (the plugin's `fields` registry). */\nexport type FieldTypeChecks = {\n normalize?: (raw: unknown, field: InputBlock) => AnswerValue\n toText?: (value: AnswerValue, field: InputBlock, words: YesNoWords) => string\n /** Return `true` when the value is fine, or the error words to show. */\n validate?: (value: AnswerValue, ctx: { answers: Answers; field: InputBlock }) => string | true\n}\n\nexport type SubmitResult =\n | {\n fieldErrors?: Record<string, string>\n message: string\n ok: false\n }\n | {\n message: string\n ok: true\n redirectURL?: string\n submissionId?: number | string\n whatsappUrl?: string\n }\n"],"names":[],"mappings":"AAAA;;CAEC,GA+LD,WAYK"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Answers, FieldTypeChecks, FormLike } from './types.js';
|
|
2
|
+
import { type TokenValue } from './tokens.js';
|
|
3
|
+
export declare const DEFAULT_WHATSAPP_TEMPLATE = "{{*}}";
|
|
4
|
+
export declare const DEFAULT_WHATSAPP_ROW_TEMPLATE = "*{{label}}:* {{value}}";
|
|
5
|
+
export type WhatsAppMessageOptions = {
|
|
6
|
+
extra?: Record<string, TokenValue>;
|
|
7
|
+
fieldTypes?: Record<string, FieldTypeChecks>;
|
|
8
|
+
/** Row format for `{{*}}`; tokens `{{label}}` and `{{value}}`. Default `*{{label}}:* {{value}}`. */
|
|
9
|
+
rowTemplate?: null | string;
|
|
10
|
+
/** Message template. Default `{{*}}`. */
|
|
11
|
+
template?: null | string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The WhatsApp message for these answers: the template with `{{*}}` as one row per answered,
|
|
15
|
+
* shown field (row labels), `{{key}}` and `{{key.attribute}}` tokens. Empty lines left by empty
|
|
16
|
+
* tokens are collapsed.
|
|
17
|
+
*/
|
|
18
|
+
export declare function buildWhatsAppMessage(form: FormLike, answers: Answers, options?: WhatsAppMessageOptions): string;
|
|
19
|
+
/** `https://wa.me/<digits>?text=<message encoded with encodeURIComponent>`. */
|
|
20
|
+
export declare function whatsappUrl(number: null | string | undefined, message: string): string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { buildMessageRows, buildTemplateValues, interpolate } from './tokens.js';
|
|
2
|
+
export const DEFAULT_WHATSAPP_TEMPLATE = '{{*}}';
|
|
3
|
+
export const DEFAULT_WHATSAPP_ROW_TEMPLATE = '*{{label}}:* {{value}}';
|
|
4
|
+
/**
|
|
5
|
+
* The WhatsApp message for these answers: the template with `{{*}}` as one row per answered,
|
|
6
|
+
* shown field (row labels), `{{key}}` and `{{key.attribute}}` tokens. Empty lines left by empty
|
|
7
|
+
* tokens are collapsed.
|
|
8
|
+
*/ export function buildWhatsAppMessage(form, answers, options = {}) {
|
|
9
|
+
const rowTemplate = options.rowTemplate?.trim() ? options.rowTemplate : DEFAULT_WHATSAPP_ROW_TEMPLATE;
|
|
10
|
+
const template = options.template?.trim() ? options.template : DEFAULT_WHATSAPP_TEMPLATE;
|
|
11
|
+
const rows = buildMessageRows(form, answers, options.fieldTypes);
|
|
12
|
+
const star = rows.map((row)=>interpolate(rowTemplate, {
|
|
13
|
+
label: row.label,
|
|
14
|
+
value: row.value
|
|
15
|
+
})).join('\n');
|
|
16
|
+
const values = buildTemplateValues(form, answers, options.extra, options.fieldTypes);
|
|
17
|
+
const withRows = template.replace(/\{\{\s*\*(?::table)?\s*\}\}/g, ()=>'\u0000ROWS\u0000');
|
|
18
|
+
const filled = interpolate(withRows, values).split('\u0000ROWS\u0000').join(star);
|
|
19
|
+
return filled.replace(/\r\n/g, '\n').split('\n').map((line)=>line.replace(/[ \t]+$/g, '')).join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
20
|
+
}
|
|
21
|
+
/** `https://wa.me/<digits>?text=<message encoded with encodeURIComponent>`. */ export function whatsappUrl(number, message) {
|
|
22
|
+
const digits = String(number ?? '').replace(/\D+/g, '');
|
|
23
|
+
return `https://wa.me/${digits}?text=${encodeURIComponent(message)}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=whatsapp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/shared/whatsapp.ts"],"sourcesContent":["import type { Answers, FieldTypeChecks, FormLike } from './types.js'\n\nimport { buildMessageRows, buildTemplateValues, interpolate, type TokenValue } from './tokens.js'\n\nexport const DEFAULT_WHATSAPP_TEMPLATE = '{{*}}'\nexport const DEFAULT_WHATSAPP_ROW_TEMPLATE = '*{{label}}:* {{value}}'\n\nexport type WhatsAppMessageOptions = {\n extra?: Record<string, TokenValue>\n fieldTypes?: Record<string, FieldTypeChecks>\n /** Row format for `{{*}}`; tokens `{{label}}` and `{{value}}`. Default `*{{label}}:* {{value}}`. */\n rowTemplate?: null | string\n /** Message template. Default `{{*}}`. */\n template?: null | string\n}\n\n/**\n * The WhatsApp message for these answers: the template with `{{*}}` as one row per answered,\n * shown field (row labels), `{{key}}` and `{{key.attribute}}` tokens. Empty lines left by empty\n * tokens are collapsed.\n */\nexport function buildWhatsAppMessage(\n form: FormLike,\n answers: Answers,\n options: WhatsAppMessageOptions = {},\n): string {\n const rowTemplate = options.rowTemplate?.trim() ? options.rowTemplate : DEFAULT_WHATSAPP_ROW_TEMPLATE\n const template = options.template?.trim() ? options.template : DEFAULT_WHATSAPP_TEMPLATE\n const rows = buildMessageRows(form, answers, options.fieldTypes)\n const star = rows.map((row) => interpolate(rowTemplate, { label: row.label, value: row.value })).join('\\n')\n const values = buildTemplateValues(form, answers, options.extra, options.fieldTypes)\n const withRows = template.replace(/\\{\\{\\s*\\*(?::table)?\\s*\\}\\}/g, () => '\\u0000ROWS\\u0000')\n const filled = interpolate(withRows, values).split('\\u0000ROWS\\u0000').join(star)\n return filled\n .replace(/\\r\\n/g, '\\n')\n .split('\\n')\n .map((line) => line.replace(/[ \\t]+$/g, ''))\n .join('\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim()\n}\n\n/** `https://wa.me/<digits>?text=<message encoded with encodeURIComponent>`. */\nexport function whatsappUrl(number: null | string | undefined, message: string): string {\n const digits = String(number ?? '').replace(/\\D+/g, '')\n return `https://wa.me/${digits}?text=${encodeURIComponent(message)}`\n}\n"],"names":["buildMessageRows","buildTemplateValues","interpolate","DEFAULT_WHATSAPP_TEMPLATE","DEFAULT_WHATSAPP_ROW_TEMPLATE","buildWhatsAppMessage","form","answers","options","rowTemplate","trim","template","rows","fieldTypes","star","map","row","label","value","join","values","extra","withRows","replace","filled","split","line","whatsappUrl","number","message","digits","String","encodeURIComponent"],"mappings":"AAEA,SAASA,gBAAgB,EAAEC,mBAAmB,EAAEC,WAAW,QAAyB,cAAa;AAEjG,OAAO,MAAMC,4BAA4B,QAAO;AAChD,OAAO,MAAMC,gCAAgC,yBAAwB;AAWrE;;;;CAIC,GACD,OAAO,SAASC,qBACdC,IAAc,EACdC,OAAgB,EAChBC,UAAkC,CAAC,CAAC;IAEpC,MAAMC,cAAcD,QAAQC,WAAW,EAAEC,SAASF,QAAQC,WAAW,GAAGL;IACxE,MAAMO,WAAWH,QAAQG,QAAQ,EAAED,SAASF,QAAQG,QAAQ,GAAGR;IAC/D,MAAMS,OAAOZ,iBAAiBM,MAAMC,SAASC,QAAQK,UAAU;IAC/D,MAAMC,OAAOF,KAAKG,GAAG,CAAC,CAACC,MAAQd,YAAYO,aAAa;YAAEQ,OAAOD,IAAIC,KAAK;YAAEC,OAAOF,IAAIE,KAAK;QAAC,IAAIC,IAAI,CAAC;IACtG,MAAMC,SAASnB,oBAAoBK,MAAMC,SAASC,QAAQa,KAAK,EAAEb,QAAQK,UAAU;IACnF,MAAMS,WAAWX,SAASY,OAAO,CAAC,gCAAgC,IAAM;IACxE,MAAMC,SAAStB,YAAYoB,UAAUF,QAAQK,KAAK,CAAC,oBAAoBN,IAAI,CAACL;IAC5E,OAAOU,OACJD,OAAO,CAAC,SAAS,MACjBE,KAAK,CAAC,MACNV,GAAG,CAAC,CAACW,OAASA,KAAKH,OAAO,CAAC,YAAY,KACvCJ,IAAI,CAAC,MACLI,OAAO,CAAC,WAAW,QACnBb,IAAI;AACT;AAEA,6EAA6E,GAC7E,OAAO,SAASiB,YAAYC,MAAiC,EAAEC,OAAe;IAC5E,MAAMC,SAASC,OAAOH,UAAU,IAAIL,OAAO,CAAC,QAAQ;IACpD,OAAO,CAAC,cAAc,EAAEO,OAAO,MAAM,EAAEE,mBAAmBH,UAAU;AACtE"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FormWords } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Starting values of every "Form wording" field. Apps change them per form in Payload, or for
|
|
4
|
+
* new forms with the plugin's `words` option. `getPublicForm` uses them only for empty fields.
|
|
5
|
+
*/
|
|
6
|
+
export declare const DEFAULT_WORDS: Readonly<FormWords>;
|
|
7
|
+
export declare const WORD_KEYS: Array<keyof FormWords>;
|
|
8
|
+
/** Fills every empty word from `defaults` (default: `DEFAULT_WORDS`). */
|
|
9
|
+
export declare function fillWords(words: null | Partial<Record<keyof FormWords, null | string>> | undefined, defaults?: Partial<FormWords>): FormWords;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Starting values of every "Form wording" field. Apps change them per form in Payload, or for
|
|
3
|
+
* new forms with the plugin's `words` option. `getPublicForm` uses them only for empty fields.
|
|
4
|
+
*/ export const DEFAULT_WORDS = Object.freeze({
|
|
5
|
+
back: 'Back',
|
|
6
|
+
chooseOne: 'Choose one',
|
|
7
|
+
closed: 'This form is closed.',
|
|
8
|
+
continue: 'Continue',
|
|
9
|
+
errorPrefix: 'Please check the fields marked below.',
|
|
10
|
+
fileRejected: '{{label}}: this file cannot be accepted.',
|
|
11
|
+
genericError: 'Something went wrong. Please try again.',
|
|
12
|
+
invalid: '{{label}} is not valid.',
|
|
13
|
+
no: 'No',
|
|
14
|
+
notFound: 'This form could not be found.',
|
|
15
|
+
optionalHint: '(optional)',
|
|
16
|
+
required: '{{label}} is required.',
|
|
17
|
+
securityCheckFailed: 'The security check failed. Please try again.',
|
|
18
|
+
send: 'Send',
|
|
19
|
+
sending: 'Sending…',
|
|
20
|
+
sent: 'Sent',
|
|
21
|
+
stepProgress: 'Step {{current}} of {{total}}',
|
|
22
|
+
success: 'Thanks. Your submission has been received.',
|
|
23
|
+
tooManyTries: 'Too many tries. Please wait a few minutes and try again.',
|
|
24
|
+
yes: 'Yes'
|
|
25
|
+
});
|
|
26
|
+
export const WORD_KEYS = Object.keys(DEFAULT_WORDS);
|
|
27
|
+
/** Fills every empty word from `defaults` (default: `DEFAULT_WORDS`). */ export function fillWords(words, defaults = DEFAULT_WORDS) {
|
|
28
|
+
const out = {
|
|
29
|
+
...DEFAULT_WORDS,
|
|
30
|
+
...defaults
|
|
31
|
+
};
|
|
32
|
+
for (const key of WORD_KEYS){
|
|
33
|
+
const value = words?.[key];
|
|
34
|
+
if (typeof value === 'string' && value.trim()) {
|
|
35
|
+
out[key] = value;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
//# sourceMappingURL=words.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/shared/words.ts"],"sourcesContent":["import type { FormWords } from './types.js'\n\n/**\n * Starting values of every \"Form wording\" field. Apps change them per form in Payload, or for\n * new forms with the plugin's `words` option. `getPublicForm` uses them only for empty fields.\n */\nexport const DEFAULT_WORDS: Readonly<FormWords> = Object.freeze({\n back: 'Back',\n chooseOne: 'Choose one',\n closed: 'This form is closed.',\n continue: 'Continue',\n errorPrefix: 'Please check the fields marked below.',\n fileRejected: '{{label}}: this file cannot be accepted.',\n genericError: 'Something went wrong. Please try again.',\n invalid: '{{label}} is not valid.',\n no: 'No',\n notFound: 'This form could not be found.',\n optionalHint: '(optional)',\n required: '{{label}} is required.',\n securityCheckFailed: 'The security check failed. Please try again.',\n send: 'Send',\n sending: 'Sending…',\n sent: 'Sent',\n stepProgress: 'Step {{current}} of {{total}}',\n success: 'Thanks. Your submission has been received.',\n tooManyTries: 'Too many tries. Please wait a few minutes and try again.',\n yes: 'Yes',\n})\n\nexport const WORD_KEYS = Object.keys(DEFAULT_WORDS) as Array<keyof FormWords>\n\n/** Fills every empty word from `defaults` (default: `DEFAULT_WORDS`). */\nexport function fillWords(\n words: null | Partial<Record<keyof FormWords, null | string>> | undefined,\n defaults: Partial<FormWords> = DEFAULT_WORDS,\n): FormWords {\n const out = { ...DEFAULT_WORDS, ...defaults } as FormWords\n for (const key of WORD_KEYS) {\n const value = words?.[key]\n if (typeof value === 'string' && value.trim()) {\n out[key] = value\n }\n }\n return out\n}\n"],"names":["DEFAULT_WORDS","Object","freeze","back","chooseOne","closed","continue","errorPrefix","fileRejected","genericError","invalid","no","notFound","optionalHint","required","securityCheckFailed","send","sending","sent","stepProgress","success","tooManyTries","yes","WORD_KEYS","keys","fillWords","words","defaults","out","key","value","trim"],"mappings":"AAEA;;;CAGC,GACD,OAAO,MAAMA,gBAAqCC,OAAOC,MAAM,CAAC;IAC9DC,MAAM;IACNC,WAAW;IACXC,QAAQ;IACRC,UAAU;IACVC,aAAa;IACbC,cAAc;IACdC,cAAc;IACdC,SAAS;IACTC,IAAI;IACJC,UAAU;IACVC,cAAc;IACdC,UAAU;IACVC,qBAAqB;IACrBC,MAAM;IACNC,SAAS;IACTC,MAAM;IACNC,cAAc;IACdC,SAAS;IACTC,cAAc;IACdC,KAAK;AACP,GAAE;AAEF,OAAO,MAAMC,YAAYtB,OAAOuB,IAAI,CAACxB,eAAwC;AAE7E,uEAAuE,GACvE,OAAO,SAASyB,UACdC,KAAyE,EACzEC,WAA+B3B,aAAa;IAE5C,MAAM4B,MAAM;QAAE,GAAG5B,aAAa;QAAE,GAAG2B,QAAQ;IAAC;IAC5C,KAAK,MAAME,OAAON,UAAW;QAC3B,MAAMO,QAAQJ,OAAO,CAACG,IAAI;QAC1B,IAAI,OAAOC,UAAU,YAAYA,MAAMC,IAAI,IAAI;YAC7CH,GAAG,CAACC,IAAI,GAAGC;QACb;IACF;IACA,OAAOF;AACT"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { Block, CollectionConfig, CollectionSlug, Field, PayloadRequest, SendEmailOptions } from 'payload';
|
|
2
|
+
import type { Answers, FieldTypeChecks, FormWords, PublicForm } from './shared/types.js';
|
|
3
|
+
/** A new field type: its block plus the checks the server runs for it. */
|
|
4
|
+
export type FieldTypeDefinition = {
|
|
5
|
+
/** The admin block. The plugin adds the common input fields (key, required, words, rules…). */
|
|
6
|
+
block: Block;
|
|
7
|
+
} & FieldTypeChecks;
|
|
8
|
+
export type CollectionOverride = {
|
|
9
|
+
/** Receives the plugin's fields and returns the field list to use. */
|
|
10
|
+
fields?: (args: {
|
|
11
|
+
defaultFields: Field[];
|
|
12
|
+
}) => Field[];
|
|
13
|
+
} & Partial<Omit<CollectionConfig, 'fields'>>;
|
|
14
|
+
export type OptionSource = {
|
|
15
|
+
/** Extra fields of each item carried as option attributes (rules and tokens read them). */
|
|
16
|
+
attributes?: string[];
|
|
17
|
+
collection: CollectionSlug;
|
|
18
|
+
/** Field shown to the visitor. */
|
|
19
|
+
labelField: string;
|
|
20
|
+
/** Field stored as the answer. */
|
|
21
|
+
valueField: string;
|
|
22
|
+
};
|
|
23
|
+
/** What a `webhooks.signingSecret` or `webhooks.headers` function receives for each webhook it is asked about. */
|
|
24
|
+
export type WebhookConfigArgs = {
|
|
25
|
+
form: PublicForm;
|
|
26
|
+
req: PayloadRequest;
|
|
27
|
+
webhook: {
|
|
28
|
+
id: null | string;
|
|
29
|
+
label: null | string;
|
|
30
|
+
url: string;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
/** A value, or a (possibly async) function of the form, request and webhook returning one. */
|
|
34
|
+
export type WebhookConfigValue<T> = ((args: WebhookConfigArgs) => Promise<T | undefined> | T | undefined) | T;
|
|
35
|
+
export type WebhookOptions = {
|
|
36
|
+
/**
|
|
37
|
+
* Request headers for every webhook, e.g. an API key from an env var. The webhook's own admin
|
|
38
|
+
* secret headers replace a config header of the same name (any case); neither can replace
|
|
39
|
+
* `Content-Type`'s place first or the signature headers.
|
|
40
|
+
*/
|
|
41
|
+
headers?: WebhookConfigValue<Record<string, string>>;
|
|
42
|
+
/**
|
|
43
|
+
* `true` refuses to send a webhook that has no signing secret from either source: no request is
|
|
44
|
+
* made and its delivery is logged as failed. Default `false`: it is sent unsigned, with one
|
|
45
|
+
* warning per webhook per server process.
|
|
46
|
+
*/
|
|
47
|
+
requireSignature?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Default `true`: each webhook row has the admin-only `signingSecret` and secret `headers`
|
|
50
|
+
* fields, as in 0.1.0. `false` leaves both fields out of the forms collection, so secrets come
|
|
51
|
+
* only from this config and none is stored in the database. Turning it off drops the column
|
|
52
|
+
* and the headers table at the app's next migration; stored values are lost.
|
|
53
|
+
*/
|
|
54
|
+
secretsInAdmin?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Signs any webhook whose admin signing secret is empty (the admin secret wins). A string, or a
|
|
57
|
+
* function returning one, e.g. per webhook label. A `whsec_…` secret also keys the Standard
|
|
58
|
+
* Webhooks signature with its decoded bytes.
|
|
59
|
+
*/
|
|
60
|
+
signingSecret?: WebhookConfigValue<string>;
|
|
61
|
+
};
|
|
62
|
+
export type FormsPluginOptions = {
|
|
63
|
+
/**
|
|
64
|
+
* Who may read and write webhook secrets in the admin, update or delete submissions and resend.
|
|
65
|
+
* Default: any logged-in user. Prefer `webhooks.signingSecret` / `webhooks.headers` from env
|
|
66
|
+
* vars (with `webhooks.secretsInAdmin: false`) over secrets stored in forms.
|
|
67
|
+
*/
|
|
68
|
+
access?: {
|
|
69
|
+
isAdmin?: (req: PayloadRequest) => boolean;
|
|
70
|
+
};
|
|
71
|
+
/** Overrides for the attachments collection (when the plugin creates it). */
|
|
72
|
+
attachmentOverrides?: CollectionOverride;
|
|
73
|
+
/** Private uploads for file fields. `false`: no attachments collection (file uploads can't be turned on). */
|
|
74
|
+
attachments?: {
|
|
75
|
+
/** Use the app's own upload collection instead of creating one; the plugin adds its link fields. */
|
|
76
|
+
collection?: CollectionSlug;
|
|
77
|
+
/** Largest file in bytes. Default 10 MB. */
|
|
78
|
+
maxFileSize?: number;
|
|
79
|
+
/** Largest total per submission in bytes. Default 25 MB. */
|
|
80
|
+
maxTotalSize?: number;
|
|
81
|
+
/** Allowed MIME types. Default: PDF, JPEG, PNG, WebP, text, CSV, Word. */
|
|
82
|
+
mimeTypes?: string[];
|
|
83
|
+
} | false;
|
|
84
|
+
/** Gets each email before it is sent (wrap it in a layout, add headers…) and returns the one to send. */
|
|
85
|
+
beforeEmail?: (message: SendEmailOptions, ctx: {
|
|
86
|
+
answers: Answers;
|
|
87
|
+
form: PublicForm;
|
|
88
|
+
req: PayloadRequest;
|
|
89
|
+
}) => Promise<SendEmailOptions> | SendEmailOptions;
|
|
90
|
+
delivery?: {
|
|
91
|
+
/**
|
|
92
|
+
* Runs email and webhook delivery after the reply, e.g. `(task) => after(task)` (Next.js) or
|
|
93
|
+
* `(task) => ctx.waitUntil(task())` (Workers). Default: delivery runs before the reply.
|
|
94
|
+
*/
|
|
95
|
+
schedule?: (task: () => Promise<void>) => void;
|
|
96
|
+
};
|
|
97
|
+
/** Default `true`. `false` returns the config untouched. */
|
|
98
|
+
enabled?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Field-type registry keyed by type: `true`/`false` turns a built-in type on or off, a partial
|
|
101
|
+
* block is merged into the built-in one, a full definition adds a new type. `fileUpload` is off
|
|
102
|
+
* unless turned on.
|
|
103
|
+
*/
|
|
104
|
+
fields?: Record<string, boolean | FieldTypeDefinition | Partial<Block>>;
|
|
105
|
+
/** Overrides for the forms collection. */
|
|
106
|
+
formOverrides?: CollectionOverride;
|
|
107
|
+
/** Name of the honeypot field in a submission. Default `_hp`. */
|
|
108
|
+
honeypotField?: string;
|
|
109
|
+
/** What a submission keeps about the request. Default: IP off, user agent on. */
|
|
110
|
+
metadata?: {
|
|
111
|
+
ip?: boolean;
|
|
112
|
+
userAgent?: boolean;
|
|
113
|
+
};
|
|
114
|
+
/** Collections whose items can be the choices of select, radio and checkbox-group fields. */
|
|
115
|
+
optionSources?: Record<string, OptionSource>;
|
|
116
|
+
/** Per form and visitor. Default 5 per 600 s. `false` turns it off. */
|
|
117
|
+
rateLimit?: {
|
|
118
|
+
max?: number;
|
|
119
|
+
windowSeconds?: number;
|
|
120
|
+
} | false;
|
|
121
|
+
/** Collection slugs. Default `custom-forms`, `custom-form-submissions`, `custom-form-attachments`. */
|
|
122
|
+
slugs?: {
|
|
123
|
+
attachments?: string;
|
|
124
|
+
forms?: string;
|
|
125
|
+
submissions?: string;
|
|
126
|
+
};
|
|
127
|
+
/** Overrides for the submissions collection. */
|
|
128
|
+
submissionOverrides?: CollectionOverride;
|
|
129
|
+
/** Cloudflare Turnstile checked on the server. On with no secret refuses every submission. */
|
|
130
|
+
turnstile?: {
|
|
131
|
+
enabled?: boolean;
|
|
132
|
+
fieldName?: string;
|
|
133
|
+
secretKey?: string;
|
|
134
|
+
};
|
|
135
|
+
/** Webhook secrets and headers from the config, and whether signing is required. */
|
|
136
|
+
webhooks?: WebhookOptions;
|
|
137
|
+
whatsapp?: {
|
|
138
|
+
/** Number for forms that have none. */
|
|
139
|
+
number?: (args: {
|
|
140
|
+
form: PublicForm;
|
|
141
|
+
req: PayloadRequest;
|
|
142
|
+
}) => Promise<string | undefined> | string | undefined;
|
|
143
|
+
};
|
|
144
|
+
/** Default words for new forms, merged over `DEFAULT_WORDS`. */
|
|
145
|
+
words?: Partial<FormWords>;
|
|
146
|
+
};
|
|
147
|
+
/** The options with every default applied. */
|
|
148
|
+
export type ResolvedOptions = {
|
|
149
|
+
attachments: {
|
|
150
|
+
collection: string;
|
|
151
|
+
createCollection: boolean;
|
|
152
|
+
maxFileSize: number;
|
|
153
|
+
maxTotalSize: number;
|
|
154
|
+
mimeTypes: string[];
|
|
155
|
+
} | null;
|
|
156
|
+
fieldTypes: Record<string, FieldTypeChecks>;
|
|
157
|
+
honeypotField: string;
|
|
158
|
+
isAdmin: (req: PayloadRequest) => boolean;
|
|
159
|
+
metadata: {
|
|
160
|
+
ip: boolean;
|
|
161
|
+
userAgent: boolean;
|
|
162
|
+
};
|
|
163
|
+
rateLimit: {
|
|
164
|
+
max: number;
|
|
165
|
+
windowSeconds: number;
|
|
166
|
+
} | null;
|
|
167
|
+
slugs: {
|
|
168
|
+
attachments: string;
|
|
169
|
+
forms: string;
|
|
170
|
+
submissions: string;
|
|
171
|
+
};
|
|
172
|
+
turnstile: {
|
|
173
|
+
enabled: boolean;
|
|
174
|
+
fieldName: string;
|
|
175
|
+
secretKey: string;
|
|
176
|
+
};
|
|
177
|
+
webhooks: {
|
|
178
|
+
requireSignature: boolean;
|
|
179
|
+
secretsInAdmin: boolean;
|
|
180
|
+
} & Pick<WebhookOptions, 'headers' | 'signingSecret'>;
|
|
181
|
+
words: FormWords;
|
|
182
|
+
} & Pick<FormsPluginOptions, 'beforeEmail' | 'delivery' | 'optionSources' | 'whatsapp'>;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type {\n Block,\n CollectionConfig,\n CollectionSlug,\n Field,\n PayloadRequest,\n SendEmailOptions,\n} from 'payload'\n\nimport type { Answers, FieldTypeChecks, FormWords, PublicForm } from './shared/types.js'\n\n/** A new field type: its block plus the checks the server runs for it. */\nexport type FieldTypeDefinition = {\n /** The admin block. The plugin adds the common input fields (key, required, words, rules…). */\n block: Block\n} & FieldTypeChecks\n\nexport type CollectionOverride = {\n /** Receives the plugin's fields and returns the field list to use. */\n fields?: (args: { defaultFields: Field[] }) => Field[]\n} & Partial<Omit<CollectionConfig, 'fields'>>\n\nexport type OptionSource = {\n /** Extra fields of each item carried as option attributes (rules and tokens read them). */\n attributes?: string[]\n collection: CollectionSlug\n /** Field shown to the visitor. */\n labelField: string\n /** Field stored as the answer. */\n valueField: string\n}\n\n/** What a `webhooks.signingSecret` or `webhooks.headers` function receives for each webhook it is asked about. */\nexport type WebhookConfigArgs = {\n form: PublicForm\n req: PayloadRequest\n webhook: { id: null | string; label: null | string; url: string }\n}\n\n/** A value, or a (possibly async) function of the form, request and webhook returning one. */\nexport type WebhookConfigValue<T> =\n | ((args: WebhookConfigArgs) => Promise<T | undefined> | T | undefined)\n | T\n\nexport type WebhookOptions = {\n /**\n * Request headers for every webhook, e.g. an API key from an env var. The webhook's own admin\n * secret headers replace a config header of the same name (any case); neither can replace\n * `Content-Type`'s place first or the signature headers.\n */\n headers?: WebhookConfigValue<Record<string, string>>\n /**\n * `true` refuses to send a webhook that has no signing secret from either source: no request is\n * made and its delivery is logged as failed. Default `false`: it is sent unsigned, with one\n * warning per webhook per server process.\n */\n requireSignature?: boolean\n /**\n * Default `true`: each webhook row has the admin-only `signingSecret` and secret `headers`\n * fields, as in 0.1.0. `false` leaves both fields out of the forms collection, so secrets come\n * only from this config and none is stored in the database. Turning it off drops the column\n * and the headers table at the app's next migration; stored values are lost.\n */\n secretsInAdmin?: boolean\n /**\n * Signs any webhook whose admin signing secret is empty (the admin secret wins). A string, or a\n * function returning one, e.g. per webhook label. A `whsec_…` secret also keys the Standard\n * Webhooks signature with its decoded bytes.\n */\n signingSecret?: WebhookConfigValue<string>\n}\n\nexport type FormsPluginOptions = {\n /**\n * Who may read and write webhook secrets in the admin, update or delete submissions and resend.\n * Default: any logged-in user. Prefer `webhooks.signingSecret` / `webhooks.headers` from env\n * vars (with `webhooks.secretsInAdmin: false`) over secrets stored in forms.\n */\n access?: { isAdmin?: (req: PayloadRequest) => boolean }\n /** Overrides for the attachments collection (when the plugin creates it). */\n attachmentOverrides?: CollectionOverride\n /** Private uploads for file fields. `false`: no attachments collection (file uploads can't be turned on). */\n attachments?:\n | {\n /** Use the app's own upload collection instead of creating one; the plugin adds its link fields. */\n collection?: CollectionSlug\n /** Largest file in bytes. Default 10 MB. */\n maxFileSize?: number\n /** Largest total per submission in bytes. Default 25 MB. */\n maxTotalSize?: number\n /** Allowed MIME types. Default: PDF, JPEG, PNG, WebP, text, CSV, Word. */\n mimeTypes?: string[]\n }\n | false\n /** Gets each email before it is sent (wrap it in a layout, add headers…) and returns the one to send. */\n beforeEmail?: (\n message: SendEmailOptions,\n ctx: { answers: Answers; form: PublicForm; req: PayloadRequest },\n ) => Promise<SendEmailOptions> | SendEmailOptions\n delivery?: {\n /**\n * Runs email and webhook delivery after the reply, e.g. `(task) => after(task)` (Next.js) or\n * `(task) => ctx.waitUntil(task())` (Workers). Default: delivery runs before the reply.\n */\n schedule?: (task: () => Promise<void>) => void\n }\n /** Default `true`. `false` returns the config untouched. */\n enabled?: boolean\n /**\n * Field-type registry keyed by type: `true`/`false` turns a built-in type on or off, a partial\n * block is merged into the built-in one, a full definition adds a new type. `fileUpload` is off\n * unless turned on.\n */\n fields?: Record<string, boolean | FieldTypeDefinition | Partial<Block>>\n /** Overrides for the forms collection. */\n formOverrides?: CollectionOverride\n /** Name of the honeypot field in a submission. Default `_hp`. */\n honeypotField?: string\n /** What a submission keeps about the request. Default: IP off, user agent on. */\n metadata?: { ip?: boolean; userAgent?: boolean }\n /** Collections whose items can be the choices of select, radio and checkbox-group fields. */\n optionSources?: Record<string, OptionSource>\n /** Per form and visitor. Default 5 per 600 s. `false` turns it off. */\n rateLimit?: { max?: number; windowSeconds?: number } | false\n /** Collection slugs. Default `custom-forms`, `custom-form-submissions`, `custom-form-attachments`. */\n slugs?: { attachments?: string; forms?: string; submissions?: string }\n /** Overrides for the submissions collection. */\n submissionOverrides?: CollectionOverride\n /** Cloudflare Turnstile checked on the server. On with no secret refuses every submission. */\n turnstile?: { enabled?: boolean; fieldName?: string; secretKey?: string }\n /** Webhook secrets and headers from the config, and whether signing is required. */\n webhooks?: WebhookOptions\n whatsapp?: {\n /** Number for forms that have none. */\n number?: (args: {\n form: PublicForm\n req: PayloadRequest\n }) => Promise<string | undefined> | string | undefined\n }\n /** Default words for new forms, merged over `DEFAULT_WORDS`. */\n words?: Partial<FormWords>\n}\n\n/** The options with every default applied. */\nexport type ResolvedOptions = {\n attachments: {\n collection: string\n createCollection: boolean\n maxFileSize: number\n maxTotalSize: number\n mimeTypes: string[]\n } | null\n fieldTypes: Record<string, FieldTypeChecks>\n honeypotField: string\n isAdmin: (req: PayloadRequest) => boolean\n metadata: { ip: boolean; userAgent: boolean }\n rateLimit: { max: number; windowSeconds: number } | null\n slugs: { attachments: string; forms: string; submissions: string }\n turnstile: { enabled: boolean; fieldName: string; secretKey: string }\n webhooks: {\n requireSignature: boolean\n secretsInAdmin: boolean\n } & Pick<WebhookOptions, 'headers' | 'signingSecret'>\n words: FormWords\n} & Pick<FormsPluginOptions, 'beforeEmail' | 'delivery' | 'optionSources' | 'whatsapp'>\n"],"names":[],"mappings":"AA+IA,4CAA4C,GAC5C,WAoBuF"}
|