@mobilizehub/payload-plugin 0.5.3 → 0.6.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.
@@ -1,4 +1,6 @@
1
1
  import { authenticated } from '../../access/authenticated.js';
2
+ import { createProcessFormSubmissionHook } from './hooks/processFormSubmission.js';
3
+ import { createSendAutoresponseHook } from './hooks/sendAutoresponse.js';
2
4
  export const generateFormSubmissionsCollection = (formSubmissionsConfig)=>{
3
5
  const defaultFields = [
4
6
  {
@@ -53,7 +55,12 @@ export const generateFormSubmissionsCollection = (formSubmissionsConfig)=>{
53
55
  hooks: {
54
56
  ...formSubmissionsConfig.formSubmissionsOverrides?.hooks || {},
55
57
  afterChange: [
58
+ createSendAutoresponseHook(formSubmissionsConfig),
56
59
  ...formSubmissionsConfig.formSubmissionsOverrides?.hooks?.afterChange || []
60
+ ],
61
+ beforeChange: [
62
+ createProcessFormSubmissionHook(formSubmissionsConfig),
63
+ ...formSubmissionsConfig.formSubmissionsOverrides?.hooks?.beforeChange || []
57
64
  ]
58
65
  }
59
66
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/collections/form-submissions/generateFormSubmissionsCollection.ts"],"sourcesContent":["import type { CollectionConfig, Field } from 'payload'\n\nimport type { MobilizehubPluginConfig } from '../../types/index.js'\n\nimport { authenticated } from '../../access/authenticated.js'\n\nexport const generateFormSubmissionsCollection = (\n formSubmissionsConfig: MobilizehubPluginConfig,\n) => {\n const defaultFields: Field[] = [\n {\n name: 'form',\n type: 'relationship',\n admin: {\n position: 'sidebar',\n readOnly: true,\n },\n relationTo: 'forms',\n required: true,\n },\n {\n name: 'contact',\n type: 'relationship',\n admin: {\n position: 'sidebar',\n readOnly: true,\n },\n relationTo: 'contacts',\n },\n {\n name: 'createdAt',\n type: 'date',\n admin: {\n position: 'sidebar',\n readOnly: true,\n },\n },\n {\n name: 'data',\n type: 'json',\n label: 'Data',\n },\n ]\n\n const config: CollectionConfig = {\n ...(formSubmissionsConfig.formSubmissionsOverrides || {}),\n slug: formSubmissionsConfig.formSubmissionsOverrides?.slug || 'formSubmissions',\n access: {\n create: () => false,\n read: authenticated,\n update: () => false,\n ...(formSubmissionsConfig.formSubmissionsOverrides?.access || {}),\n },\n admin: {\n ...(formSubmissionsConfig.formSubmissionsOverrides?.admin || {}),\n hidden: formSubmissionsConfig.formSubmissionsOverrides?.admin?.hidden || true,\n },\n fields: formSubmissionsConfig.formSubmissionsOverrides?.fields\n ? formSubmissionsConfig.formSubmissionsOverrides.fields({ defaultFields })\n : defaultFields,\n hooks: {\n ...(formSubmissionsConfig.formSubmissionsOverrides?.hooks || {}),\n afterChange: [...(formSubmissionsConfig.formSubmissionsOverrides?.hooks?.afterChange || [])],\n },\n }\n\n return config\n}\n"],"names":["authenticated","generateFormSubmissionsCollection","formSubmissionsConfig","defaultFields","name","type","admin","position","readOnly","relationTo","required","label","config","formSubmissionsOverrides","slug","access","create","read","update","hidden","fields","hooks","afterChange"],"mappings":"AAIA,SAASA,aAAa,QAAQ,gCAA+B;AAE7D,OAAO,MAAMC,oCAAoC,CAC/CC;IAEA,MAAMC,gBAAyB;QAC7B;YACEC,MAAM;YACNC,MAAM;YACNC,OAAO;gBACLC,UAAU;gBACVC,UAAU;YACZ;YACAC,YAAY;YACZC,UAAU;QACZ;QACA;YACEN,MAAM;YACNC,MAAM;YACNC,OAAO;gBACLC,UAAU;gBACVC,UAAU;YACZ;YACAC,YAAY;QACd;QACA;YACEL,MAAM;YACNC,MAAM;YACNC,OAAO;gBACLC,UAAU;gBACVC,UAAU;YACZ;QACF;QACA;YACEJ,MAAM;YACNC,MAAM;YACNM,OAAO;QACT;KACD;IAED,MAAMC,SAA2B;QAC/B,GAAIV,sBAAsBW,wBAAwB,IAAI,CAAC,CAAC;QACxDC,MAAMZ,sBAAsBW,wBAAwB,EAAEC,QAAQ;QAC9DC,QAAQ;YACNC,QAAQ,IAAM;YACdC,MAAMjB;YACNkB,QAAQ,IAAM;YACd,GAAIhB,sBAAsBW,wBAAwB,EAAEE,UAAU,CAAC,CAAC;QAClE;QACAT,OAAO;YACL,GAAIJ,sBAAsBW,wBAAwB,EAAEP,SAAS,CAAC,CAAC;YAC/Da,QAAQjB,sBAAsBW,wBAAwB,EAAEP,OAAOa,UAAU;QAC3E;QACAC,QAAQlB,sBAAsBW,wBAAwB,EAAEO,SACpDlB,sBAAsBW,wBAAwB,CAACO,MAAM,CAAC;YAAEjB;QAAc,KACtEA;QACJkB,OAAO;YACL,GAAInB,sBAAsBW,wBAAwB,EAAEQ,SAAS,CAAC,CAAC;YAC/DC,aAAa;mBAAKpB,sBAAsBW,wBAAwB,EAAEQ,OAAOC,eAAe,EAAE;aAAE;QAC9F;IACF;IAEA,OAAOV;AACT,EAAC"}
1
+ {"version":3,"sources":["../../../src/collections/form-submissions/generateFormSubmissionsCollection.ts"],"sourcesContent":["import type { CollectionConfig, Field } from 'payload'\n\nimport type { MobilizehubPluginConfig } from '../../types/index.js'\n\nimport { authenticated } from '../../access/authenticated.js'\nimport { createProcessFormSubmissionHook } from './hooks/processFormSubmission.js'\nimport { createSendAutoresponseHook } from './hooks/sendAutoresponse.js'\n\nexport const generateFormSubmissionsCollection = (\n formSubmissionsConfig: MobilizehubPluginConfig,\n) => {\n const defaultFields: Field[] = [\n {\n name: 'form',\n type: 'relationship',\n admin: {\n position: 'sidebar',\n readOnly: true,\n },\n relationTo: 'forms',\n required: true,\n },\n {\n name: 'contact',\n type: 'relationship',\n admin: {\n position: 'sidebar',\n readOnly: true,\n },\n relationTo: 'contacts',\n },\n {\n name: 'createdAt',\n type: 'date',\n admin: {\n position: 'sidebar',\n readOnly: true,\n },\n },\n {\n name: 'data',\n type: 'json',\n label: 'Data',\n },\n ]\n\n const config: CollectionConfig = {\n ...(formSubmissionsConfig.formSubmissionsOverrides || {}),\n slug: formSubmissionsConfig.formSubmissionsOverrides?.slug || 'formSubmissions',\n access: {\n create: () => false,\n read: authenticated,\n update: () => false,\n ...(formSubmissionsConfig.formSubmissionsOverrides?.access || {}),\n },\n admin: {\n ...(formSubmissionsConfig.formSubmissionsOverrides?.admin || {}),\n hidden: formSubmissionsConfig.formSubmissionsOverrides?.admin?.hidden || true,\n },\n fields: formSubmissionsConfig.formSubmissionsOverrides?.fields\n ? formSubmissionsConfig.formSubmissionsOverrides.fields({ defaultFields })\n : defaultFields,\n hooks: {\n ...(formSubmissionsConfig.formSubmissionsOverrides?.hooks || {}),\n afterChange: [\n createSendAutoresponseHook(formSubmissionsConfig),\n ...(formSubmissionsConfig.formSubmissionsOverrides?.hooks?.afterChange || []),\n ],\n beforeChange: [\n createProcessFormSubmissionHook(formSubmissionsConfig),\n ...(formSubmissionsConfig.formSubmissionsOverrides?.hooks?.beforeChange || []),\n ],\n },\n }\n\n return config\n}\n"],"names":["authenticated","createProcessFormSubmissionHook","createSendAutoresponseHook","generateFormSubmissionsCollection","formSubmissionsConfig","defaultFields","name","type","admin","position","readOnly","relationTo","required","label","config","formSubmissionsOverrides","slug","access","create","read","update","hidden","fields","hooks","afterChange","beforeChange"],"mappings":"AAIA,SAASA,aAAa,QAAQ,gCAA+B;AAC7D,SAASC,+BAA+B,QAAQ,mCAAkC;AAClF,SAASC,0BAA0B,QAAQ,8BAA6B;AAExE,OAAO,MAAMC,oCAAoC,CAC/CC;IAEA,MAAMC,gBAAyB;QAC7B;YACEC,MAAM;YACNC,MAAM;YACNC,OAAO;gBACLC,UAAU;gBACVC,UAAU;YACZ;YACAC,YAAY;YACZC,UAAU;QACZ;QACA;YACEN,MAAM;YACNC,MAAM;YACNC,OAAO;gBACLC,UAAU;gBACVC,UAAU;YACZ;YACAC,YAAY;QACd;QACA;YACEL,MAAM;YACNC,MAAM;YACNC,OAAO;gBACLC,UAAU;gBACVC,UAAU;YACZ;QACF;QACA;YACEJ,MAAM;YACNC,MAAM;YACNM,OAAO;QACT;KACD;IAED,MAAMC,SAA2B;QAC/B,GAAIV,sBAAsBW,wBAAwB,IAAI,CAAC,CAAC;QACxDC,MAAMZ,sBAAsBW,wBAAwB,EAAEC,QAAQ;QAC9DC,QAAQ;YACNC,QAAQ,IAAM;YACdC,MAAMnB;YACNoB,QAAQ,IAAM;YACd,GAAIhB,sBAAsBW,wBAAwB,EAAEE,UAAU,CAAC,CAAC;QAClE;QACAT,OAAO;YACL,GAAIJ,sBAAsBW,wBAAwB,EAAEP,SAAS,CAAC,CAAC;YAC/Da,QAAQjB,sBAAsBW,wBAAwB,EAAEP,OAAOa,UAAU;QAC3E;QACAC,QAAQlB,sBAAsBW,wBAAwB,EAAEO,SACpDlB,sBAAsBW,wBAAwB,CAACO,MAAM,CAAC;YAAEjB;QAAc,KACtEA;QACJkB,OAAO;YACL,GAAInB,sBAAsBW,wBAAwB,EAAEQ,SAAS,CAAC,CAAC;YAC/DC,aAAa;gBACXtB,2BAA2BE;mBACvBA,sBAAsBW,wBAAwB,EAAEQ,OAAOC,eAAe,EAAE;aAC7E;YACDC,cAAc;gBACZxB,gCAAgCG;mBAC5BA,sBAAsBW,wBAAwB,EAAEQ,OAAOE,gBAAgB,EAAE;aAC9E;QACH;IACF;IAEA,OAAOX;AACT,EAAC"}
@@ -0,0 +1,11 @@
1
+ import type { CollectionBeforeChangeHook } from 'payload';
2
+ import type { MobilizehubPluginConfig } from '../../../types/index.js';
3
+ /**
4
+ * Creates the form submission processing hook.
5
+ *
6
+ * This hook:
7
+ * 1. Creates or updates a contact based on submission data
8
+ * 2. Applies form tags to the contact
9
+ * 3. Links the submission to the contact by returning modified data
10
+ */
11
+ export declare const createProcessFormSubmissionHook: (pluginConfig: MobilizehubPluginConfig) => CollectionBeforeChangeHook;
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Contact field mapping from form submission data to contact fields.
3
+ */ const CONTACT_FIELD_MAP = [
4
+ 'email',
5
+ 'emailOptIn',
6
+ 'firstName',
7
+ 'lastName',
8
+ 'mobileNumber',
9
+ 'mobileOptIn',
10
+ 'address',
11
+ 'city',
12
+ 'state',
13
+ 'zip',
14
+ 'country'
15
+ ];
16
+ /**
17
+ * Extracts contact-related fields from form submission data.
18
+ */ function extractContactData(submissionData) {
19
+ const contactData = {};
20
+ for (const field of CONTACT_FIELD_MAP){
21
+ if (submissionData[field] !== undefined) {
22
+ contactData[field] = submissionData[field];
23
+ }
24
+ }
25
+ return contactData;
26
+ }
27
+ /**
28
+ * Creates the form submission processing hook.
29
+ *
30
+ * This hook:
31
+ * 1. Creates or updates a contact based on submission data
32
+ * 2. Applies form tags to the contact
33
+ * 3. Links the submission to the contact by returning modified data
34
+ */ export const createProcessFormSubmissionHook = (pluginConfig)=>{
35
+ const contactsSlug = pluginConfig.contactsOverrides?.slug || 'contacts';
36
+ const formsSlug = pluginConfig.formsOverrides?.slug || 'forms';
37
+ return async ({ data, operation, req })=>{
38
+ // Only process on creation
39
+ if (operation !== 'create') {
40
+ return data;
41
+ }
42
+ const { payload } = req;
43
+ const logger = payload.logger;
44
+ // Parse submission data
45
+ const submissionData = data.data;
46
+ if (!submissionData) {
47
+ logger.warn('Form submission has no data');
48
+ return data;
49
+ }
50
+ // Email is required for contact creation
51
+ const email = submissionData.email;
52
+ if (!email || typeof email !== 'string') {
53
+ logger.info('Form submission has no email, skipping contact creation');
54
+ return data;
55
+ }
56
+ try {
57
+ // Extract contact fields from submission
58
+ const contactData = extractContactData(submissionData);
59
+ // Find or create contact
60
+ const existingContactResult = await payload.find({
61
+ collection: contactsSlug,
62
+ limit: 1,
63
+ where: {
64
+ email: {
65
+ equals: email
66
+ }
67
+ }
68
+ });
69
+ const existingContact = existingContactResult.docs[0];
70
+ let contactId;
71
+ if (existingContact) {
72
+ // Update existing contact (merge data, don't overwrite with empty values)
73
+ const updateData = {};
74
+ for (const [key, value] of Object.entries(contactData)){
75
+ if (value !== undefined && value !== null && value !== '') {
76
+ updateData[key] = value;
77
+ }
78
+ }
79
+ await payload.update({
80
+ id: existingContact.id,
81
+ collection: contactsSlug,
82
+ data: updateData
83
+ });
84
+ contactId = existingContact.id;
85
+ logger.info(`Updated contact ${contactId} from form submission`);
86
+ } else {
87
+ // Create new contact
88
+ const newContact = await payload.create({
89
+ collection: contactsSlug,
90
+ data: contactData
91
+ });
92
+ contactId = newContact.id;
93
+ logger.info(`Created contact ${contactId} from form submission`);
94
+ }
95
+ const formId = data.form;
96
+ const formIdValue = typeof formId === 'object' ? formId.id : formId;
97
+ if (!formIdValue) {
98
+ logger.error('Form ID not found for form submission');
99
+ throw new Error('Form ID not found for form submission');
100
+ }
101
+ // Get form to check for tags
102
+ const form = await payload.findByID({
103
+ id: formIdValue,
104
+ collection: formsSlug,
105
+ depth: 0
106
+ });
107
+ // Apply form tags to contact
108
+ if (form && Array.isArray(form.tags) && form.tags.length > 0) {
109
+ // Get form tag IDs
110
+ const formTagIds = form.tags.map((t)=>typeof t === 'object' ? t.id : t);
111
+ // Fetch current contact to get up-to-date tags
112
+ const currentContact = await payload.findByID({
113
+ id: contactId,
114
+ collection: contactsSlug,
115
+ depth: 0
116
+ });
117
+ // Get existing tag IDs (normalize to IDs)
118
+ const existingTagIds = (currentContact.tags || []).map((t)=>typeof t === 'object' ? t.id : t);
119
+ // Find only new tags that don't already exist
120
+ const newTagIds = formTagIds.filter((tagId)=>!existingTagIds.includes(tagId));
121
+ if (newTagIds.length > 0) {
122
+ // Merge tags
123
+ const mergedTags = [
124
+ ...existingTagIds,
125
+ ...newTagIds
126
+ ];
127
+ // Update contact with merged tags
128
+ await payload.update({
129
+ id: contactId,
130
+ collection: contactsSlug,
131
+ data: {
132
+ tags: mergedTags
133
+ }
134
+ });
135
+ logger.info(`Applied ${newTagIds.length} new tags to contact ${contactId}`);
136
+ } else {
137
+ logger.info(`Contact ${contactId} already has all form tags`);
138
+ }
139
+ }
140
+ // Return data with contact linked - no separate update needed
141
+ return {
142
+ ...data,
143
+ contact: contactId
144
+ };
145
+ } catch (error) {
146
+ logger.error(error, 'Error processing form submission');
147
+ // Don't throw - we don't want to fail the submission
148
+ return data;
149
+ }
150
+ };
151
+ };
152
+
153
+ //# sourceMappingURL=processFormSubmission.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/collections/form-submissions/hooks/processFormSubmission.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport type { MobilizehubPluginConfig } from '../../../types/index.js'\n\n/**\n * Contact field mapping from form submission data to contact fields.\n */\nconst CONTACT_FIELD_MAP = [\n 'email',\n 'emailOptIn',\n 'firstName',\n 'lastName',\n 'mobileNumber',\n 'mobileOptIn',\n 'address',\n 'city',\n 'state',\n 'zip',\n 'country',\n] as const\n\ntype ContactFieldName = (typeof CONTACT_FIELD_MAP)[number]\n\n/**\n * Extracts contact-related fields from form submission data.\n */\nfunction extractContactData(\n submissionData: Record<string, unknown>,\n): Partial<Record<ContactFieldName, unknown>> {\n const contactData: Partial<Record<ContactFieldName, unknown>> = {}\n\n for (const field of CONTACT_FIELD_MAP) {\n if (submissionData[field] !== undefined) {\n contactData[field] = submissionData[field]\n }\n }\n\n return contactData\n}\n\n/**\n * Creates the form submission processing hook.\n *\n * This hook:\n * 1. Creates or updates a contact based on submission data\n * 2. Applies form tags to the contact\n * 3. Links the submission to the contact by returning modified data\n */\nexport const createProcessFormSubmissionHook = (\n pluginConfig: MobilizehubPluginConfig,\n): CollectionBeforeChangeHook => {\n const contactsSlug = pluginConfig.contactsOverrides?.slug || 'contacts'\n const formsSlug = pluginConfig.formsOverrides?.slug || 'forms'\n\n return async ({ data, operation, req }) => {\n // Only process on creation\n if (operation !== 'create') {\n return data\n }\n\n const { payload } = req\n const logger = payload.logger\n\n // Parse submission data\n const submissionData = data.data as Record<string, unknown> | undefined\n\n if (!submissionData) {\n logger.warn('Form submission has no data')\n return data\n }\n\n // Email is required for contact creation\n const email = submissionData.email as string | undefined\n\n if (!email || typeof email !== 'string') {\n logger.info('Form submission has no email, skipping contact creation')\n return data\n }\n\n try {\n // Extract contact fields from submission\n const contactData = extractContactData(submissionData)\n\n // Find or create contact\n const existingContactResult = await payload.find({\n collection: contactsSlug,\n limit: 1,\n where: { email: { equals: email } },\n })\n\n const existingContact = existingContactResult.docs[0] as\n | { id: number | string; tags?: (number | string)[] }\n | undefined\n\n let contactId: number | string\n\n if (existingContact) {\n // Update existing contact (merge data, don't overwrite with empty values)\n const updateData: Record<string, unknown> = {}\n\n for (const [key, value] of Object.entries(contactData)) {\n if (value !== undefined && value !== null && value !== '') {\n updateData[key] = value\n }\n }\n\n await payload.update({\n id: existingContact.id,\n collection: contactsSlug,\n data: updateData,\n })\n\n contactId = existingContact.id\n logger.info(`Updated contact ${contactId} from form submission`)\n } else {\n // Create new contact\n const newContact = await payload.create({\n collection: contactsSlug,\n data: contactData,\n })\n\n contactId = newContact.id\n logger.info(`Created contact ${contactId} from form submission`)\n }\n\n const formId = data.form as { id: number | string } | number | string\n const formIdValue = typeof formId === 'object' ? formId.id : formId\n\n if (!formIdValue) {\n logger.error('Form ID not found for form submission')\n throw new Error('Form ID not found for form submission')\n }\n\n // Get form to check for tags\n const form = await payload.findByID({\n id: formIdValue,\n collection: formsSlug,\n depth: 0,\n })\n\n // Apply form tags to contact\n if (form && Array.isArray(form.tags) && form.tags.length > 0) {\n // Get form tag IDs\n const formTagIds = form.tags.map((t: { id: number | string } | number | string) =>\n typeof t === 'object' ? t.id : t,\n )\n\n // Fetch current contact to get up-to-date tags\n const currentContact = await payload.findByID({\n id: contactId,\n collection: contactsSlug,\n depth: 0,\n })\n\n // Get existing tag IDs (normalize to IDs)\n const existingTagIds = (\n (currentContact.tags as ({ id: number | string } | number | string)[]) || []\n ).map((t) => (typeof t === 'object' ? t.id : t))\n\n // Find only new tags that don't already exist\n const newTagIds = formTagIds.filter(\n (tagId: number | string) => !existingTagIds.includes(tagId),\n )\n\n if (newTagIds.length > 0) {\n // Merge tags\n const mergedTags = [...existingTagIds, ...newTagIds]\n\n // Update contact with merged tags\n await payload.update({\n id: contactId,\n collection: contactsSlug,\n data: { tags: mergedTags },\n })\n\n logger.info(`Applied ${newTagIds.length} new tags to contact ${contactId}`)\n } else {\n logger.info(`Contact ${contactId} already has all form tags`)\n }\n }\n\n // Return data with contact linked - no separate update needed\n return {\n ...data,\n contact: contactId,\n }\n } catch (error) {\n logger.error(error as Error, 'Error processing form submission')\n // Don't throw - we don't want to fail the submission\n return data\n }\n }\n}\n"],"names":["CONTACT_FIELD_MAP","extractContactData","submissionData","contactData","field","undefined","createProcessFormSubmissionHook","pluginConfig","contactsSlug","contactsOverrides","slug","formsSlug","formsOverrides","data","operation","req","payload","logger","warn","email","info","existingContactResult","find","collection","limit","where","equals","existingContact","docs","contactId","updateData","key","value","Object","entries","update","id","newContact","create","formId","form","formIdValue","error","Error","findByID","depth","Array","isArray","tags","length","formTagIds","map","t","currentContact","existingTagIds","newTagIds","filter","tagId","includes","mergedTags","contact"],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { CollectionAfterChangeHook } from 'payload';
2
+ import type { MobilizehubPluginConfig } from '../../../types/index.js';
3
+ /**
4
+ * Creates the autoresponse email hook for form submissions.
5
+ *
6
+ * This hook sends an automatic confirmation email to the form submitter
7
+ * when autoresponse is enabled on the form. It:
8
+ * 1. Checks if autoresponse is enabled on the form
9
+ * 2. Validates required autoresponse fields
10
+ * 3. Parses Lexical content to HTML
11
+ * 4. Renders through email template
12
+ * 5. Sends the email
13
+ *
14
+ * Errors are logged but never thrown to avoid failing the submission.
15
+ */
16
+ export declare const createSendAutoresponseHook: (pluginConfig: MobilizehubPluginConfig) => CollectionAfterChangeHook;
@@ -0,0 +1,109 @@
1
+ import { formatFromAddress } from '../../../utils/email.js';
2
+ import { parseLexicalContent } from '../../../utils/lexical.js';
3
+ /**
4
+ * Creates the autoresponse email hook for form submissions.
5
+ *
6
+ * This hook sends an automatic confirmation email to the form submitter
7
+ * when autoresponse is enabled on the form. It:
8
+ * 1. Checks if autoresponse is enabled on the form
9
+ * 2. Validates required autoresponse fields
10
+ * 3. Parses Lexical content to HTML
11
+ * 4. Renders through email template
12
+ * 5. Sends the email
13
+ *
14
+ * Errors are logged but never thrown to avoid failing the submission.
15
+ */ export const createSendAutoresponseHook = (pluginConfig)=>{
16
+ const formsSlug = pluginConfig.formsOverrides?.slug || 'forms';
17
+ const contactsSlug = pluginConfig.contactsOverrides?.slug || 'contacts';
18
+ return async ({ doc, operation, req })=>{
19
+ // Only process on create
20
+ if (operation !== 'create') {
21
+ return doc;
22
+ }
23
+ const { payload } = req;
24
+ const logger = payload.logger;
25
+ try {
26
+ // Get form ID from submission
27
+ const formId = doc.form;
28
+ const formIdValue = typeof formId === 'object' ? formId.id : formId;
29
+ if (!formIdValue) {
30
+ return doc;
31
+ }
32
+ // Fetch form to get autoresponse configuration
33
+ const form = await payload.findByID({
34
+ id: formIdValue,
35
+ collection: formsSlug
36
+ });
37
+ if (!form) {
38
+ return doc;
39
+ }
40
+ // Check if autoresponse is enabled
41
+ const autoresponse = form.autoresponse;
42
+ if (!autoresponse?.enabled) {
43
+ return doc;
44
+ }
45
+ // Get contact email
46
+ const contactId = doc.contact;
47
+ if (!contactId) {
48
+ logger.warn('Form submission has no linked contact, skipping autoresponse');
49
+ return doc;
50
+ }
51
+ const contactIdValue = typeof contactId === 'object' ? contactId.id : contactId;
52
+ const contact = await payload.findByID({
53
+ id: contactIdValue,
54
+ collection: contactsSlug
55
+ });
56
+ const contactEmail = contact?.email;
57
+ if (!contactEmail) {
58
+ logger.warn(`Contact ${contactIdValue} has no email, skipping autoresponse`);
59
+ return doc;
60
+ }
61
+ // Validate required autoresponse fields
62
+ const { content, fromAddress, fromName, previewText, replyTo, subject } = autoresponse;
63
+ if (!subject) {
64
+ logger.warn(`Form ${formIdValue} autoresponse has no subject, skipping`);
65
+ return doc;
66
+ }
67
+ if (!content) {
68
+ logger.warn(`Form ${formIdValue} autoresponse has no content, skipping`);
69
+ return doc;
70
+ }
71
+ if (!fromName || !fromAddress) {
72
+ logger.warn(`Form ${formIdValue} autoresponse has no from address, skipping`);
73
+ return doc;
74
+ }
75
+ // Get email adapter
76
+ const { render, sendEmail } = pluginConfig.email(req);
77
+ // Parse Lexical content
78
+ const parsedContent = await parseLexicalContent(content, payload.config);
79
+ // Format from address
80
+ const formattedFromAddress = formatFromAddress(fromName, fromAddress);
81
+ // Render through email template
82
+ const html = render({
83
+ from: formattedFromAddress,
84
+ html: parsedContent.html,
85
+ markdown: parsedContent.markdown,
86
+ plainText: parsedContent.plainText,
87
+ previewText,
88
+ replyTo,
89
+ subject,
90
+ to: contactEmail,
91
+ token: ''
92
+ });
93
+ // Send the email
94
+ await sendEmail({
95
+ from: formattedFromAddress,
96
+ html,
97
+ subject,
98
+ to: contactEmail
99
+ });
100
+ logger.info(`Sent autoresponse to ${contactEmail} for form ${formIdValue}`);
101
+ } catch (error) {
102
+ logger.error(error, 'Error sending autoresponse email');
103
+ // Don't throw - submission should still succeed
104
+ }
105
+ return doc;
106
+ };
107
+ };
108
+
109
+ //# sourceMappingURL=sendAutoresponse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/collections/form-submissions/hooks/sendAutoresponse.ts"],"sourcesContent":["import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'\nimport type { CollectionAfterChangeHook } from 'payload'\n\nimport type { MobilizehubPluginConfig } from '../../../types/index.js'\n\nimport { formatFromAddress } from '../../../utils/email.js'\nimport { parseLexicalContent } from '../../../utils/lexical.js'\n\n/**\n * Autoresponse configuration from form.\n */\ntype AutoresponseConfig = {\n content?: SerializedEditorState\n enabled?: boolean\n fromAddress?: string\n fromName?: string\n previewText?: string\n replyTo?: string\n subject?: string\n}\n\n/**\n * Creates the autoresponse email hook for form submissions.\n *\n * This hook sends an automatic confirmation email to the form submitter\n * when autoresponse is enabled on the form. It:\n * 1. Checks if autoresponse is enabled on the form\n * 2. Validates required autoresponse fields\n * 3. Parses Lexical content to HTML\n * 4. Renders through email template\n * 5. Sends the email\n *\n * Errors are logged but never thrown to avoid failing the submission.\n */\nexport const createSendAutoresponseHook = (\n pluginConfig: MobilizehubPluginConfig,\n): CollectionAfterChangeHook => {\n const formsSlug = pluginConfig.formsOverrides?.slug || 'forms'\n const contactsSlug = pluginConfig.contactsOverrides?.slug || 'contacts'\n\n return async ({ doc, operation, req }) => {\n // Only process on create\n if (operation !== 'create') {\n return doc\n }\n\n const { payload } = req\n const logger = payload.logger\n\n try {\n // Get form ID from submission\n const formId = doc.form as { id: number | string } | number | string\n const formIdValue = typeof formId === 'object' ? formId.id : formId\n\n if (!formIdValue) {\n return doc\n }\n\n // Fetch form to get autoresponse configuration\n const form = await payload.findByID({\n id: formIdValue,\n collection: formsSlug,\n })\n\n if (!form) {\n return doc\n }\n\n // Check if autoresponse is enabled\n const autoresponse = form.autoresponse as AutoresponseConfig | undefined\n\n if (!autoresponse?.enabled) {\n return doc\n }\n\n // Get contact email\n const contactId = doc.contact as { id: number | string } | number | string | undefined\n\n if (!contactId) {\n logger.warn('Form submission has no linked contact, skipping autoresponse')\n return doc\n }\n\n const contactIdValue = typeof contactId === 'object' ? contactId.id : contactId\n\n const contact = await payload.findByID({\n id: contactIdValue,\n collection: contactsSlug,\n })\n\n const contactEmail = contact?.email as string | undefined\n\n if (!contactEmail) {\n logger.warn(`Contact ${contactIdValue} has no email, skipping autoresponse`)\n return doc\n }\n\n // Validate required autoresponse fields\n const { content, fromAddress, fromName, previewText, replyTo, subject } = autoresponse\n\n if (!subject) {\n logger.warn(`Form ${formIdValue} autoresponse has no subject, skipping`)\n return doc\n }\n\n if (!content) {\n logger.warn(`Form ${formIdValue} autoresponse has no content, skipping`)\n return doc\n }\n\n if (!fromName || !fromAddress) {\n logger.warn(`Form ${formIdValue} autoresponse has no from address, skipping`)\n return doc\n }\n\n // Get email adapter\n const { render, sendEmail } = pluginConfig.email(req)\n\n // Parse Lexical content\n const parsedContent = await parseLexicalContent(content, payload.config)\n\n // Format from address\n const formattedFromAddress = formatFromAddress(fromName, fromAddress)\n\n // Render through email template\n const html = render({\n from: formattedFromAddress,\n html: parsedContent.html,\n markdown: parsedContent.markdown,\n plainText: parsedContent.plainText,\n previewText,\n replyTo,\n subject,\n to: contactEmail,\n token: '', // No unsubscribe token for autoresponse\n })\n\n // Send the email\n await sendEmail({\n from: formattedFromAddress,\n html,\n subject,\n to: contactEmail,\n })\n\n logger.info(`Sent autoresponse to ${contactEmail} for form ${formIdValue}`)\n } catch (error) {\n logger.error(error as Error, 'Error sending autoresponse email')\n // Don't throw - submission should still succeed\n }\n\n return doc\n }\n}\n"],"names":["formatFromAddress","parseLexicalContent","createSendAutoresponseHook","pluginConfig","formsSlug","formsOverrides","slug","contactsSlug","contactsOverrides","doc","operation","req","payload","logger","formId","form","formIdValue","id","findByID","collection","autoresponse","enabled","contactId","contact","warn","contactIdValue","contactEmail","email","content","fromAddress","fromName","previewText","replyTo","subject","render","sendEmail","parsedContent","config","formattedFromAddress","html","from","markdown","plainText","to","token","info","error"],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { PayloadHandler } from 'payload';
2
+ import type { MobilizehubPluginConfig } from '../types/index.js';
3
+ /**
4
+ * Creates the public form submission endpoint handler.
5
+ *
6
+ * Accepts form submissions from frontend applications, validates the data,
7
+ * creates a form submission record, and returns the appropriate confirmation.
8
+ *
9
+ * This endpoint is public (no authentication required) but validates:
10
+ * - Form exists and is published
11
+ * - Required fields are present
12
+ * - Email format is valid (if provided)
13
+ */
14
+ export declare const formSubmissionHandler: (pluginConfig: MobilizehubPluginConfig) => PayloadHandler;
@@ -0,0 +1,146 @@
1
+ import z from 'zod';
2
+ import { ErrorCodes, errorResponse, successResponse } from '../utils/api-response.js';
3
+ /**
4
+ * Schema for form submission request body.
5
+ */ const FormSubmissionBodySchema = z.object({
6
+ data: z.record(z.string(), z.unknown()),
7
+ formId: z.union([
8
+ z.string(),
9
+ z.number()
10
+ ])
11
+ });
12
+ /**
13
+ * Validates submission data against form field configuration.
14
+ */ function validateSubmissionData(data, contactFields) {
15
+ const errors = [];
16
+ if (!contactFields || contactFields.length === 0) {
17
+ return {
18
+ errors: [],
19
+ valid: true
20
+ };
21
+ }
22
+ for (const field of contactFields){
23
+ if (field.required) {
24
+ const value = data[field.blockType];
25
+ if (value === undefined || value === null || value === '') {
26
+ errors.push(`${field.blockType} is required`);
27
+ }
28
+ }
29
+ }
30
+ // Validate email format if present
31
+ if (data.email && typeof data.email === 'string') {
32
+ const emailRegex = /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/;
33
+ if (!emailRegex.test(data.email)) {
34
+ errors.push('Invalid email format');
35
+ }
36
+ }
37
+ return {
38
+ errors,
39
+ valid: errors.length === 0
40
+ };
41
+ }
42
+ /**
43
+ * Fetches and validates a form exists and is published.
44
+ */ async function getPublishedForm(payload, formId, collectionSlug) {
45
+ try {
46
+ const form = await payload.findByID({
47
+ id: formId,
48
+ collection: collectionSlug
49
+ });
50
+ if (!form) {
51
+ return null;
52
+ }
53
+ // Only allow submissions to published forms
54
+ if (form.status !== 'published') {
55
+ return null;
56
+ }
57
+ return form;
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+ /**
63
+ * Builds the confirmation response based on form settings.
64
+ */ function buildConfirmationResponse(form) {
65
+ if (form.confirmationType === 'redirect' && form.reference) {
66
+ let redirectUrl;
67
+ if (form.url) {
68
+ redirectUrl = form.url;
69
+ } else if (typeof form.reference.value === 'object' && form.reference.value.slug) {
70
+ redirectUrl = `/${form.reference.value.slug}`;
71
+ }
72
+ if (redirectUrl) {
73
+ return {
74
+ type: 'redirect',
75
+ redirect: redirectUrl
76
+ };
77
+ }
78
+ }
79
+ return {
80
+ type: 'message',
81
+ message: form.confirmationMessage || 'Thank you for your submission.'
82
+ };
83
+ }
84
+ /**
85
+ * Creates the public form submission endpoint handler.
86
+ *
87
+ * Accepts form submissions from frontend applications, validates the data,
88
+ * creates a form submission record, and returns the appropriate confirmation.
89
+ *
90
+ * This endpoint is public (no authentication required) but validates:
91
+ * - Form exists and is published
92
+ * - Required fields are present
93
+ * - Email format is valid (if provided)
94
+ */ export const formSubmissionHandler = (pluginConfig)=>{
95
+ const formsSlug = pluginConfig.formsOverrides?.slug || 'forms';
96
+ const formSubmissionsSlug = pluginConfig.formSubmissionsOverrides?.slug || 'formSubmissions';
97
+ return async (req)=>{
98
+ const { payload } = req;
99
+ const logger = payload.logger;
100
+ if (!req.json) {
101
+ return errorResponse(ErrorCodes.BAD_REQUEST, 'No JSON body provided', 400);
102
+ }
103
+ try {
104
+ const body = await req.json();
105
+ // Validate request body structure
106
+ const parseResult = FormSubmissionBodySchema.safeParse(body);
107
+ if (!parseResult.success) {
108
+ const firstError = parseResult.error.issues[0]?.message || 'Invalid request body';
109
+ return errorResponse(ErrorCodes.VALIDATION_ERROR, firstError, 400);
110
+ }
111
+ const { data, formId } = parseResult.data;
112
+ // Fetch and validate form
113
+ const form = await getPublishedForm(payload, formId, formsSlug);
114
+ if (!form) {
115
+ return errorResponse(ErrorCodes.NOT_FOUND, 'Form not found or not published', 404);
116
+ }
117
+ // Validate submission data against form fields
118
+ const validation = validateSubmissionData(data, form.contactFields);
119
+ if (!validation.valid) {
120
+ return errorResponse(ErrorCodes.VALIDATION_ERROR, validation.errors.join(', '), 400);
121
+ }
122
+ // Create the form submission
123
+ // Note: The beforeChange hook will handle contact creation/update
124
+ const submission = await payload.create({
125
+ collection: formSubmissionsSlug,
126
+ data: {
127
+ data,
128
+ form: form.id
129
+ },
130
+ // Use internal context to bypass access control
131
+ overrideAccess: true
132
+ });
133
+ // Build confirmation response
134
+ const confirmation = buildConfirmationResponse(form);
135
+ return successResponse({
136
+ confirmation,
137
+ submissionId: submission.id
138
+ }, 201);
139
+ } catch (error) {
140
+ logger.error(error, 'Error processing form submission');
141
+ return errorResponse(ErrorCodes.INTERNAL_ERROR, error instanceof Error ? error.message : 'Failed to process submission', 500);
142
+ }
143
+ };
144
+ };
145
+
146
+ //# sourceMappingURL=formSubmissionHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/endpoints/formSubmissionHandler.ts"],"sourcesContent":["import type { CollectionSlug, Payload, PayloadHandler } from 'payload'\n\nimport z from 'zod'\n\nimport type { MobilizehubPluginConfig } from '../types/index.js'\n\nimport { ErrorCodes, errorResponse, successResponse } from '../utils/api-response.js'\n\n/**\n * Schema for form submission request body.\n */\nconst FormSubmissionBodySchema = z.object({\n data: z.record(z.string(), z.unknown()),\n formId: z.union([z.string(), z.number()]),\n})\n\n/**\n * Form type for validation.\n */\ntype FormDocument = {\n confirmationMessage?: unknown\n confirmationType?: 'message' | 'redirect'\n contactFields?: Array<{\n blockType: string\n required?: boolean\n }>\n id: number | string\n reference?: {\n relationTo: 'forms' | 'pages'\n value: { slug?: string } | number | string\n }\n status?: 'draft' | 'published'\n url?: string\n}\n\n/**\n * Validates submission data against form field configuration.\n */\nfunction validateSubmissionData(\n data: Record<string, unknown>,\n contactFields: FormDocument['contactFields'],\n): { errors: string[]; valid: boolean } {\n const errors: string[] = []\n\n if (!contactFields || contactFields.length === 0) {\n return { errors: [], valid: true }\n }\n\n for (const field of contactFields) {\n if (field.required) {\n const value = data[field.blockType]\n if (value === undefined || value === null || value === '') {\n errors.push(`${field.blockType} is required`)\n }\n }\n }\n\n // Validate email format if present\n if (data.email && typeof data.email === 'string') {\n const emailRegex = /^[^\\s@]+@[^\\s@][^\\s.@]*\\.[^\\s@]+$/\n if (!emailRegex.test(data.email)) {\n errors.push('Invalid email format')\n }\n }\n\n return {\n errors,\n valid: errors.length === 0,\n }\n}\n\n/**\n * Fetches and validates a form exists and is published.\n */\nasync function getPublishedForm(\n payload: Payload,\n formId: number | string,\n collectionSlug: CollectionSlug,\n): Promise<FormDocument | null> {\n try {\n const form = await payload.findByID({\n id: formId,\n collection: collectionSlug,\n })\n\n if (!form) {\n return null\n }\n\n // Only allow submissions to published forms\n if ((form as FormDocument).status !== 'published') {\n return null\n }\n\n return form as FormDocument\n } catch {\n return null\n }\n}\n\n/**\n * Builds the confirmation response based on form settings.\n */\nfunction buildConfirmationResponse(form: FormDocument): {\n message?: unknown\n redirect?: string\n type: 'message' | 'redirect'\n} {\n if (form.confirmationType === 'redirect' && form.reference) {\n let redirectUrl: string | undefined\n\n if (form.url) {\n redirectUrl = form.url\n } else if (typeof form.reference.value === 'object' && form.reference.value.slug) {\n redirectUrl = `/${form.reference.value.slug}`\n }\n\n if (redirectUrl) {\n return { type: 'redirect', redirect: redirectUrl }\n }\n }\n\n return {\n type: 'message',\n message: form.confirmationMessage || 'Thank you for your submission.',\n }\n}\n\n/**\n * Creates the public form submission endpoint handler.\n *\n * Accepts form submissions from frontend applications, validates the data,\n * creates a form submission record, and returns the appropriate confirmation.\n *\n * This endpoint is public (no authentication required) but validates:\n * - Form exists and is published\n * - Required fields are present\n * - Email format is valid (if provided)\n */\nexport const formSubmissionHandler = (\n pluginConfig: MobilizehubPluginConfig,\n): PayloadHandler => {\n const formsSlug = pluginConfig.formsOverrides?.slug || 'forms'\n const formSubmissionsSlug = pluginConfig.formSubmissionsOverrides?.slug || 'formSubmissions'\n\n return async (req) => {\n const { payload } = req\n const logger = payload.logger\n\n if (!req.json) {\n return errorResponse(ErrorCodes.BAD_REQUEST, 'No JSON body provided', 400)\n }\n\n try {\n const body = await req.json()\n\n // Validate request body structure\n const parseResult = FormSubmissionBodySchema.safeParse(body)\n\n if (!parseResult.success) {\n const firstError = parseResult.error.issues[0]?.message || 'Invalid request body'\n return errorResponse(ErrorCodes.VALIDATION_ERROR, firstError, 400)\n }\n\n const { data, formId } = parseResult.data\n\n // Fetch and validate form\n const form = await getPublishedForm(payload, formId, formsSlug)\n\n if (!form) {\n return errorResponse(ErrorCodes.NOT_FOUND, 'Form not found or not published', 404)\n }\n\n // Validate submission data against form fields\n const validation = validateSubmissionData(data, form.contactFields)\n\n if (!validation.valid) {\n return errorResponse(ErrorCodes.VALIDATION_ERROR, validation.errors.join(', '), 400)\n }\n\n // Create the form submission\n // Note: The beforeChange hook will handle contact creation/update\n const submission = await payload.create({\n collection: formSubmissionsSlug,\n data: {\n data,\n form: form.id as number,\n },\n // Use internal context to bypass access control\n overrideAccess: true,\n })\n\n // Build confirmation response\n const confirmation = buildConfirmationResponse(form)\n\n return successResponse(\n {\n confirmation,\n submissionId: submission.id,\n },\n 201,\n )\n } catch (error) {\n logger.error(error as Error, 'Error processing form submission')\n return errorResponse(\n ErrorCodes.INTERNAL_ERROR,\n error instanceof Error ? error.message : 'Failed to process submission',\n 500,\n )\n }\n }\n}\n"],"names":["z","ErrorCodes","errorResponse","successResponse","FormSubmissionBodySchema","object","data","record","string","unknown","formId","union","number","validateSubmissionData","contactFields","errors","length","valid","field","required","value","blockType","undefined","push","email","emailRegex","test","getPublishedForm","payload","collectionSlug","form","findByID","id","collection","status","buildConfirmationResponse","confirmationType","reference","redirectUrl","url","slug","type","redirect","message","confirmationMessage","formSubmissionHandler","pluginConfig","formsSlug","formsOverrides","formSubmissionsSlug","formSubmissionsOverrides","req","logger","json","BAD_REQUEST","body","parseResult","safeParse","success","firstError","error","issues","VALIDATION_ERROR","NOT_FOUND","validation","join","submission","create","overrideAccess","confirmation","submissionId","INTERNAL_ERROR","Error"],"mappings":"AAEA,OAAOA,OAAO,MAAK;AAInB,SAASC,UAAU,EAAEC,aAAa,EAAEC,eAAe,QAAQ,2BAA0B;AAErF;;CAEC,GACD,MAAMC,2BAA2BJ,EAAEK,MAAM,CAAC;IACxCC,MAAMN,EAAEO,MAAM,CAACP,EAAEQ,MAAM,IAAIR,EAAES,OAAO;IACpCC,QAAQV,EAAEW,KAAK,CAAC;QAACX,EAAEQ,MAAM;QAAIR,EAAEY,MAAM;KAAG;AAC1C;AAqBA;;CAEC,GACD,SAASC,uBACPP,IAA6B,EAC7BQ,aAA4C;IAE5C,MAAMC,SAAmB,EAAE;IAE3B,IAAI,CAACD,iBAAiBA,cAAcE,MAAM,KAAK,GAAG;QAChD,OAAO;YAAED,QAAQ,EAAE;YAAEE,OAAO;QAAK;IACnC;IAEA,KAAK,MAAMC,SAASJ,cAAe;QACjC,IAAII,MAAMC,QAAQ,EAAE;YAClB,MAAMC,QAAQd,IAAI,CAACY,MAAMG,SAAS,CAAC;YACnC,IAAID,UAAUE,aAAaF,UAAU,QAAQA,UAAU,IAAI;gBACzDL,OAAOQ,IAAI,CAAC,GAAGL,MAAMG,SAAS,CAAC,YAAY,CAAC;YAC9C;QACF;IACF;IAEA,mCAAmC;IACnC,IAAIf,KAAKkB,KAAK,IAAI,OAAOlB,KAAKkB,KAAK,KAAK,UAAU;QAChD,MAAMC,aAAa;QACnB,IAAI,CAACA,WAAWC,IAAI,CAACpB,KAAKkB,KAAK,GAAG;YAChCT,OAAOQ,IAAI,CAAC;QACd;IACF;IAEA,OAAO;QACLR;QACAE,OAAOF,OAAOC,MAAM,KAAK;IAC3B;AACF;AAEA;;CAEC,GACD,eAAeW,iBACbC,OAAgB,EAChBlB,MAAuB,EACvBmB,cAA8B;IAE9B,IAAI;QACF,MAAMC,OAAO,MAAMF,QAAQG,QAAQ,CAAC;YAClCC,IAAItB;YACJuB,YAAYJ;QACd;QAEA,IAAI,CAACC,MAAM;YACT,OAAO;QACT;QAEA,4CAA4C;QAC5C,IAAI,AAACA,KAAsBI,MAAM,KAAK,aAAa;YACjD,OAAO;QACT;QAEA,OAAOJ;IACT,EAAE,OAAM;QACN,OAAO;IACT;AACF;AAEA;;CAEC,GACD,SAASK,0BAA0BL,IAAkB;IAKnD,IAAIA,KAAKM,gBAAgB,KAAK,cAAcN,KAAKO,SAAS,EAAE;QAC1D,IAAIC;QAEJ,IAAIR,KAAKS,GAAG,EAAE;YACZD,cAAcR,KAAKS,GAAG;QACxB,OAAO,IAAI,OAAOT,KAAKO,SAAS,CAACjB,KAAK,KAAK,YAAYU,KAAKO,SAAS,CAACjB,KAAK,CAACoB,IAAI,EAAE;YAChFF,cAAc,CAAC,CAAC,EAAER,KAAKO,SAAS,CAACjB,KAAK,CAACoB,IAAI,EAAE;QAC/C;QAEA,IAAIF,aAAa;YACf,OAAO;gBAAEG,MAAM;gBAAYC,UAAUJ;YAAY;QACnD;IACF;IAEA,OAAO;QACLG,MAAM;QACNE,SAASb,KAAKc,mBAAmB,IAAI;IACvC;AACF;AAEA;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,wBAAwB,CACnCC;IAEA,MAAMC,YAAYD,aAAaE,cAAc,EAAER,QAAQ;IACvD,MAAMS,sBAAsBH,aAAaI,wBAAwB,EAAEV,QAAQ;IAE3E,OAAO,OAAOW;QACZ,MAAM,EAAEvB,OAAO,EAAE,GAAGuB;QACpB,MAAMC,SAASxB,QAAQwB,MAAM;QAE7B,IAAI,CAACD,IAAIE,IAAI,EAAE;YACb,OAAOnD,cAAcD,WAAWqD,WAAW,EAAE,yBAAyB;QACxE;QAEA,IAAI;YACF,MAAMC,OAAO,MAAMJ,IAAIE,IAAI;YAE3B,kCAAkC;YAClC,MAAMG,cAAcpD,yBAAyBqD,SAAS,CAACF;YAEvD,IAAI,CAACC,YAAYE,OAAO,EAAE;gBACxB,MAAMC,aAAaH,YAAYI,KAAK,CAACC,MAAM,CAAC,EAAE,EAAElB,WAAW;gBAC3D,OAAOzC,cAAcD,WAAW6D,gBAAgB,EAAEH,YAAY;YAChE;YAEA,MAAM,EAAErD,IAAI,EAAEI,MAAM,EAAE,GAAG8C,YAAYlD,IAAI;YAEzC,0BAA0B;YAC1B,MAAMwB,OAAO,MAAMH,iBAAiBC,SAASlB,QAAQqC;YAErD,IAAI,CAACjB,MAAM;gBACT,OAAO5B,cAAcD,WAAW8D,SAAS,EAAE,mCAAmC;YAChF;YAEA,+CAA+C;YAC/C,MAAMC,aAAanD,uBAAuBP,MAAMwB,KAAKhB,aAAa;YAElE,IAAI,CAACkD,WAAW/C,KAAK,EAAE;gBACrB,OAAOf,cAAcD,WAAW6D,gBAAgB,EAAEE,WAAWjD,MAAM,CAACkD,IAAI,CAAC,OAAO;YAClF;YAEA,6BAA6B;YAC7B,kEAAkE;YAClE,MAAMC,aAAa,MAAMtC,QAAQuC,MAAM,CAAC;gBACtClC,YAAYgB;gBACZ3C,MAAM;oBACJA;oBACAwB,MAAMA,KAAKE,EAAE;gBACf;gBACA,gDAAgD;gBAChDoC,gBAAgB;YAClB;YAEA,8BAA8B;YAC9B,MAAMC,eAAelC,0BAA0BL;YAE/C,OAAO3B,gBACL;gBACEkE;gBACAC,cAAcJ,WAAWlC,EAAE;YAC7B,GACA;QAEJ,EAAE,OAAO4B,OAAO;YACdR,OAAOQ,KAAK,CAACA,OAAgB;YAC7B,OAAO1D,cACLD,WAAWsE,cAAc,EACzBX,iBAAiBY,QAAQZ,MAAMjB,OAAO,GAAG,gCACzC;QAEJ;IACF;AACF,EAAC"}
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import { generatePagesCollection } from './collections/pages/generatePagesCollec
7
7
  import { generateTagsCollection } from './collections/tags/generateTagsCollection.js';
8
8
  import { generateUnsubscribeTokensCollection } from './collections/unsubscribe-tokens/generateUnsubscribeTokens.js';
9
9
  import { emailWebhookHandler } from './endpoints/emailWebhookHandler.js';
10
+ import { formSubmissionHandler } from './endpoints/formSubmissionHandler.js';
10
11
  import { sendBroadcastHandler } from './endpoints/sendBroadcastHandler.js';
11
12
  import { sendTestEmailHandler } from './endpoints/sendTestBroadcastHandler.js';
12
13
  import { unsubscribeHandler } from './endpoints/unsubscribeHandler.js';
@@ -44,6 +45,11 @@ export const mobilizehubPlugin = (pluginOptions)=>(config)=>{
44
45
  handler: emailWebhookHandler(pluginOptions),
45
46
  method: 'post',
46
47
  path: '/webhooks/email'
48
+ },
49
+ {
50
+ handler: formSubmissionHandler(pluginOptions),
51
+ method: 'post',
52
+ path: '/forms.createSubmission'
47
53
  }
48
54
  ];
49
55
  config.endpoints = [
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Config, Endpoint, TaskConfig } from 'payload'\n\nimport type { MobilizehubPluginConfig } from './types/index.js'\n\nimport { generateBroadcastsCollection } from './collections/broadcasts/generateBroadcastsCollection.js'\nimport { generateContactsCollection } from './collections/contacts/generateContactsCollection.js'\nimport { generateEmailsCollection } from './collections/emails/generateEmailsCollection.js'\nimport { generateFormSubmissionsCollection } from './collections/form-submissions/generateFormSubmissionsCollection.js'\nimport { generateFormsCollection } from './collections/forms/generateFormsCollection.js'\nimport { generatePagesCollection } from './collections/pages/generatePagesCollection.js'\nimport { generateTagsCollection } from './collections/tags/generateTagsCollection.js'\nimport { generateUnsubscribeTokensCollection } from './collections/unsubscribe-tokens/generateUnsubscribeTokens.js'\nimport { emailWebhookHandler } from './endpoints/emailWebhookHandler.js'\nimport { sendBroadcastHandler } from './endpoints/sendBroadcastHandler.js'\nimport { sendTestEmailHandler } from './endpoints/sendTestBroadcastHandler.js'\nimport { unsubscribeHandler } from './endpoints/unsubscribeHandler.js'\nimport { createSendBroadcastsTask } from './tasks/sendBroadcastsTask.js'\nimport { createSendEmailTask } from './tasks/sendEmailTask.js'\n\nexport * from './types/index.js'\n\nexport const mobilizehubPlugin =\n (pluginOptions: MobilizehubPluginConfig) =>\n (config: Config): Config => {\n if (!config.collections) {\n config.collections = []\n }\n\n config.collections.push(\n generateTagsCollection(pluginOptions),\n generateContactsCollection(pluginOptions),\n generateBroadcastsCollection(pluginOptions),\n generateEmailsCollection(pluginOptions),\n generatePagesCollection(pluginOptions),\n generateUnsubscribeTokensCollection(),\n generateFormSubmissionsCollection(pluginOptions),\n generateFormsCollection(pluginOptions),\n )\n\n if (pluginOptions.disabled) {\n return config\n }\n\n if (!config.endpoints) {\n config.endpoints = []\n }\n\n const endpoints: Endpoint[] = [\n {\n handler: sendBroadcastHandler(),\n method: 'post',\n path: '/send-broadcast',\n },\n {\n handler: sendTestEmailHandler(pluginOptions),\n method: 'post',\n path: '/send-test-email',\n },\n {\n handler: unsubscribeHandler(),\n method: 'post',\n path: '/unsubscribe',\n },\n {\n handler: emailWebhookHandler(pluginOptions),\n method: 'post',\n path: '/webhooks/email',\n },\n ]\n\n config.endpoints = [...config.endpoints, ...endpoints]\n\n if (!config.jobs) {\n config.jobs = {\n tasks: [],\n }\n }\n\n const tasks: TaskConfig[] = [\n createSendBroadcastsTask(pluginOptions),\n createSendEmailTask(pluginOptions),\n ]\n\n config.jobs.tasks = [...(config.jobs.tasks ?? []), ...tasks]\n\n const incomingOnInit = config.onInit\n\n config.onInit = async (payload) => {\n if (incomingOnInit) {\n await incomingOnInit(payload)\n }\n }\n\n return config\n }\n"],"names":["generateBroadcastsCollection","generateContactsCollection","generateEmailsCollection","generateFormSubmissionsCollection","generateFormsCollection","generatePagesCollection","generateTagsCollection","generateUnsubscribeTokensCollection","emailWebhookHandler","sendBroadcastHandler","sendTestEmailHandler","unsubscribeHandler","createSendBroadcastsTask","createSendEmailTask","mobilizehubPlugin","pluginOptions","config","collections","push","disabled","endpoints","handler","method","path","jobs","tasks","incomingOnInit","onInit","payload"],"mappings":"AAIA,SAASA,4BAA4B,QAAQ,2DAA0D;AACvG,SAASC,0BAA0B,QAAQ,uDAAsD;AACjG,SAASC,wBAAwB,QAAQ,mDAAkD;AAC3F,SAASC,iCAAiC,QAAQ,sEAAqE;AACvH,SAASC,uBAAuB,QAAQ,iDAAgD;AACxF,SAASC,uBAAuB,QAAQ,iDAAgD;AACxF,SAASC,sBAAsB,QAAQ,+CAA8C;AACrF,SAASC,mCAAmC,QAAQ,gEAA+D;AACnH,SAASC,mBAAmB,QAAQ,qCAAoC;AACxE,SAASC,oBAAoB,QAAQ,sCAAqC;AAC1E,SAASC,oBAAoB,QAAQ,0CAAyC;AAC9E,SAASC,kBAAkB,QAAQ,oCAAmC;AACtE,SAASC,wBAAwB,QAAQ,gCAA+B;AACxE,SAASC,mBAAmB,QAAQ,2BAA0B;AAE9D,cAAc,mBAAkB;AAEhC,OAAO,MAAMC,oBACX,CAACC,gBACD,CAACC;QACC,IAAI,CAACA,OAAOC,WAAW,EAAE;YACvBD,OAAOC,WAAW,GAAG,EAAE;QACzB;QAEAD,OAAOC,WAAW,CAACC,IAAI,CACrBZ,uBAAuBS,gBACvBd,2BAA2Bc,gBAC3Bf,6BAA6Be,gBAC7Bb,yBAAyBa,gBACzBV,wBAAwBU,gBACxBR,uCACAJ,kCAAkCY,gBAClCX,wBAAwBW;QAG1B,IAAIA,cAAcI,QAAQ,EAAE;YAC1B,OAAOH;QACT;QAEA,IAAI,CAACA,OAAOI,SAAS,EAAE;YACrBJ,OAAOI,SAAS,GAAG,EAAE;QACvB;QAEA,MAAMA,YAAwB;YAC5B;gBACEC,SAASZ;gBACTa,QAAQ;gBACRC,MAAM;YACR;YACA;gBACEF,SAASX,qBAAqBK;gBAC9BO,QAAQ;gBACRC,MAAM;YACR;YACA;gBACEF,SAASV;gBACTW,QAAQ;gBACRC,MAAM;YACR;YACA;gBACEF,SAASb,oBAAoBO;gBAC7BO,QAAQ;gBACRC,MAAM;YACR;SACD;QAEDP,OAAOI,SAAS,GAAG;eAAIJ,OAAOI,SAAS;eAAKA;SAAU;QAEtD,IAAI,CAACJ,OAAOQ,IAAI,EAAE;YAChBR,OAAOQ,IAAI,GAAG;gBACZC,OAAO,EAAE;YACX;QACF;QAEA,MAAMA,QAAsB;YAC1Bb,yBAAyBG;YACzBF,oBAAoBE;SACrB;QAEDC,OAAOQ,IAAI,CAACC,KAAK,GAAG;eAAKT,OAAOQ,IAAI,CAACC,KAAK,IAAI,EAAE;eAAMA;SAAM;QAE5D,MAAMC,iBAAiBV,OAAOW,MAAM;QAEpCX,OAAOW,MAAM,GAAG,OAAOC;YACrB,IAAIF,gBAAgB;gBAClB,MAAMA,eAAeE;YACvB;QACF;QAEA,OAAOZ;IACT,EAAC"}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Config, Endpoint, TaskConfig } from 'payload'\n\nimport type { MobilizehubPluginConfig } from './types/index.js'\n\nimport { generateBroadcastsCollection } from './collections/broadcasts/generateBroadcastsCollection.js'\nimport { generateContactsCollection } from './collections/contacts/generateContactsCollection.js'\nimport { generateEmailsCollection } from './collections/emails/generateEmailsCollection.js'\nimport { generateFormSubmissionsCollection } from './collections/form-submissions/generateFormSubmissionsCollection.js'\nimport { generateFormsCollection } from './collections/forms/generateFormsCollection.js'\nimport { generatePagesCollection } from './collections/pages/generatePagesCollection.js'\nimport { generateTagsCollection } from './collections/tags/generateTagsCollection.js'\nimport { generateUnsubscribeTokensCollection } from './collections/unsubscribe-tokens/generateUnsubscribeTokens.js'\nimport { emailWebhookHandler } from './endpoints/emailWebhookHandler.js'\nimport { formSubmissionHandler } from './endpoints/formSubmissionHandler.js'\nimport { sendBroadcastHandler } from './endpoints/sendBroadcastHandler.js'\nimport { sendTestEmailHandler } from './endpoints/sendTestBroadcastHandler.js'\nimport { unsubscribeHandler } from './endpoints/unsubscribeHandler.js'\nimport { createSendBroadcastsTask } from './tasks/sendBroadcastsTask.js'\nimport { createSendEmailTask } from './tasks/sendEmailTask.js'\n\nexport * from './types/index.js'\n\nexport const mobilizehubPlugin =\n (pluginOptions: MobilizehubPluginConfig) =>\n (config: Config): Config => {\n if (!config.collections) {\n config.collections = []\n }\n\n config.collections.push(\n generateTagsCollection(pluginOptions),\n generateContactsCollection(pluginOptions),\n generateBroadcastsCollection(pluginOptions),\n generateEmailsCollection(pluginOptions),\n generatePagesCollection(pluginOptions),\n generateUnsubscribeTokensCollection(),\n generateFormSubmissionsCollection(pluginOptions),\n generateFormsCollection(pluginOptions),\n )\n\n if (pluginOptions.disabled) {\n return config\n }\n\n if (!config.endpoints) {\n config.endpoints = []\n }\n\n const endpoints: Endpoint[] = [\n {\n handler: sendBroadcastHandler(),\n method: 'post',\n path: '/send-broadcast',\n },\n {\n handler: sendTestEmailHandler(pluginOptions),\n method: 'post',\n path: '/send-test-email',\n },\n {\n handler: unsubscribeHandler(),\n method: 'post',\n path: '/unsubscribe',\n },\n {\n handler: emailWebhookHandler(pluginOptions),\n method: 'post',\n path: '/webhooks/email',\n },\n {\n handler: formSubmissionHandler(pluginOptions),\n method: 'post',\n path: '/forms.createSubmission',\n },\n ]\n\n config.endpoints = [...config.endpoints, ...endpoints]\n\n if (!config.jobs) {\n config.jobs = {\n tasks: [],\n }\n }\n\n const tasks: TaskConfig[] = [\n createSendBroadcastsTask(pluginOptions),\n createSendEmailTask(pluginOptions),\n ]\n\n config.jobs.tasks = [...(config.jobs.tasks ?? []), ...tasks]\n\n const incomingOnInit = config.onInit\n\n config.onInit = async (payload) => {\n if (incomingOnInit) {\n await incomingOnInit(payload)\n }\n }\n\n return config\n }\n"],"names":["generateBroadcastsCollection","generateContactsCollection","generateEmailsCollection","generateFormSubmissionsCollection","generateFormsCollection","generatePagesCollection","generateTagsCollection","generateUnsubscribeTokensCollection","emailWebhookHandler","formSubmissionHandler","sendBroadcastHandler","sendTestEmailHandler","unsubscribeHandler","createSendBroadcastsTask","createSendEmailTask","mobilizehubPlugin","pluginOptions","config","collections","push","disabled","endpoints","handler","method","path","jobs","tasks","incomingOnInit","onInit","payload"],"mappings":"AAIA,SAASA,4BAA4B,QAAQ,2DAA0D;AACvG,SAASC,0BAA0B,QAAQ,uDAAsD;AACjG,SAASC,wBAAwB,QAAQ,mDAAkD;AAC3F,SAASC,iCAAiC,QAAQ,sEAAqE;AACvH,SAASC,uBAAuB,QAAQ,iDAAgD;AACxF,SAASC,uBAAuB,QAAQ,iDAAgD;AACxF,SAASC,sBAAsB,QAAQ,+CAA8C;AACrF,SAASC,mCAAmC,QAAQ,gEAA+D;AACnH,SAASC,mBAAmB,QAAQ,qCAAoC;AACxE,SAASC,qBAAqB,QAAQ,uCAAsC;AAC5E,SAASC,oBAAoB,QAAQ,sCAAqC;AAC1E,SAASC,oBAAoB,QAAQ,0CAAyC;AAC9E,SAASC,kBAAkB,QAAQ,oCAAmC;AACtE,SAASC,wBAAwB,QAAQ,gCAA+B;AACxE,SAASC,mBAAmB,QAAQ,2BAA0B;AAE9D,cAAc,mBAAkB;AAEhC,OAAO,MAAMC,oBACX,CAACC,gBACD,CAACC;QACC,IAAI,CAACA,OAAOC,WAAW,EAAE;YACvBD,OAAOC,WAAW,GAAG,EAAE;QACzB;QAEAD,OAAOC,WAAW,CAACC,IAAI,CACrBb,uBAAuBU,gBACvBf,2BAA2Be,gBAC3BhB,6BAA6BgB,gBAC7Bd,yBAAyBc,gBACzBX,wBAAwBW,gBACxBT,uCACAJ,kCAAkCa,gBAClCZ,wBAAwBY;QAG1B,IAAIA,cAAcI,QAAQ,EAAE;YAC1B,OAAOH;QACT;QAEA,IAAI,CAACA,OAAOI,SAAS,EAAE;YACrBJ,OAAOI,SAAS,GAAG,EAAE;QACvB;QAEA,MAAMA,YAAwB;YAC5B;gBACEC,SAASZ;gBACTa,QAAQ;gBACRC,MAAM;YACR;YACA;gBACEF,SAASX,qBAAqBK;gBAC9BO,QAAQ;gBACRC,MAAM;YACR;YACA;gBACEF,SAASV;gBACTW,QAAQ;gBACRC,MAAM;YACR;YACA;gBACEF,SAASd,oBAAoBQ;gBAC7BO,QAAQ;gBACRC,MAAM;YACR;YACA;gBACEF,SAASb,sBAAsBO;gBAC/BO,QAAQ;gBACRC,MAAM;YACR;SACD;QAEDP,OAAOI,SAAS,GAAG;eAAIJ,OAAOI,SAAS;eAAKA;SAAU;QAEtD,IAAI,CAACJ,OAAOQ,IAAI,EAAE;YAChBR,OAAOQ,IAAI,GAAG;gBACZC,OAAO,EAAE;YACX;QACF;QAEA,MAAMA,QAAsB;YAC1Bb,yBAAyBG;YACzBF,oBAAoBE;SACrB;QAEDC,OAAOQ,IAAI,CAACC,KAAK,GAAG;eAAKT,OAAOQ,IAAI,CAACC,KAAK,IAAI,EAAE;eAAMA;SAAM;QAE5D,MAAMC,iBAAiBV,OAAOW,MAAM;QAEpCX,OAAOW,MAAM,GAAG,OAAOC;YACrB,IAAIF,gBAAgB;gBAClB,MAAMA,eAAeE;YACvB;QACF;QAEA,OAAOZ;IACT,EAAC"}
@@ -1 +1,2 @@
1
+ export { submitForm } from './submit-form.js';
1
2
  export { confirmUnsubscribe } from './unsubscribe.js';
@@ -1,3 +1,4 @@
1
+ export { submitForm } from './submit-form.js';
1
2
  export { confirmUnsubscribe } from './unsubscribe.js';
2
3
 
3
4
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/react/index.ts"],"sourcesContent":["export { confirmUnsubscribe } from './unsubscribe.js'\n"],"names":["confirmUnsubscribe"],"mappings":"AAAA,SAASA,kBAAkB,QAAQ,mBAAkB"}
1
+ {"version":3,"sources":["../../src/react/index.ts"],"sourcesContent":["export { submitForm } from './submit-form.js'\nexport { confirmUnsubscribe } from './unsubscribe.js'\n"],"names":["submitForm","confirmUnsubscribe"],"mappings":"AAAA,SAASA,UAAU,QAAQ,mBAAkB;AAC7C,SAASC,kBAAkB,QAAQ,mBAAkB"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Options for handling form submission responses.
3
+ */
4
+ type SubmitFormOptions = {
5
+ /**
6
+ * Called when the form returns a message confirmation.
7
+ * @param message - The confirmation message (may be a RichText object)
8
+ */
9
+ onMessage?: (message: unknown) => void;
10
+ /**
11
+ * Called when the form should redirect after submission.
12
+ * @param redirect - The URL to redirect to
13
+ */
14
+ onRedirect?: (redirect: string) => void;
15
+ };
16
+ /**
17
+ * Submits a form to the backend API.
18
+ *
19
+ * @param args - Form submission arguments
20
+ * @param args.formId - The ID of the form to submit to
21
+ * @param args.data - The form data to submit
22
+ * @param args.opts - Optional callbacks for handling the response
23
+ * @returns The submission data including confirmation and submissionId
24
+ * @throws Error if the submission fails
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * const handleSubmit = async (formData: Record<string, unknown>) => {
29
+ * try {
30
+ * const result = await submitForm({
31
+ * formId: '1',
32
+ * data: formData,
33
+ * opts: {
34
+ * onRedirect: (url) => router.push(url),
35
+ * onMessage: (message) => setConfirmation(message),
36
+ * },
37
+ * })
38
+ * console.log('Submission ID:', result.submissionId)
39
+ * } catch (error) {
40
+ * console.error('Form submission failed:', error)
41
+ * }
42
+ * }
43
+ * ```
44
+ */
45
+ export declare function submitForm(args: {
46
+ data: Record<string, unknown>;
47
+ formId: number | string;
48
+ opts?: SubmitFormOptions;
49
+ }): Promise<{
50
+ confirmation: {
51
+ message?: unknown;
52
+ redirect?: string;
53
+ type: "message" | "redirect";
54
+ };
55
+ submissionId: number | string;
56
+ } | undefined>;
57
+ export {};
@@ -0,0 +1,54 @@
1
+ const apiUrl = '/api/forms.createSubmission';
2
+ /**
3
+ * Submits a form to the backend API.
4
+ *
5
+ * @param args - Form submission arguments
6
+ * @param args.formId - The ID of the form to submit to
7
+ * @param args.data - The form data to submit
8
+ * @param args.opts - Optional callbacks for handling the response
9
+ * @returns The submission data including confirmation and submissionId
10
+ * @throws Error if the submission fails
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * const handleSubmit = async (formData: Record<string, unknown>) => {
15
+ * try {
16
+ * const result = await submitForm({
17
+ * formId: '1',
18
+ * data: formData,
19
+ * opts: {
20
+ * onRedirect: (url) => router.push(url),
21
+ * onMessage: (message) => setConfirmation(message),
22
+ * },
23
+ * })
24
+ * console.log('Submission ID:', result.submissionId)
25
+ * } catch (error) {
26
+ * console.error('Form submission failed:', error)
27
+ * }
28
+ * }
29
+ * ```
30
+ */ export async function submitForm(args) {
31
+ const response = await fetch(apiUrl, {
32
+ body: JSON.stringify({
33
+ data: args.data,
34
+ formId: args.formId
35
+ }),
36
+ headers: {
37
+ 'Content-Type': 'application/json'
38
+ },
39
+ method: 'POST'
40
+ });
41
+ const result = await response.json();
42
+ if (!result.success) {
43
+ throw new Error(result.error?.message || 'Submission failed');
44
+ }
45
+ if (result.data?.confirmation.type === 'redirect' && result.data.confirmation.redirect) {
46
+ args.opts?.onRedirect?.(result.data.confirmation.redirect);
47
+ }
48
+ if (result.data?.confirmation.type === 'message') {
49
+ args.opts?.onMessage?.(result.data.confirmation.message);
50
+ }
51
+ return result.data;
52
+ }
53
+
54
+ //# sourceMappingURL=submit-form.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/submit-form.ts"],"sourcesContent":["const apiUrl = '/api/forms.createSubmission'\n\n/**\n * Response type from the form submission API.\n */\ntype FormSubmissionResponse = {\n data?: {\n confirmation: {\n message?: unknown\n redirect?: string\n type: 'message' | 'redirect'\n }\n submissionId: number | string\n }\n error?: {\n code: string\n message: string\n }\n success: boolean\n}\n\n/**\n * Options for handling form submission responses.\n */\ntype SubmitFormOptions = {\n /**\n * Called when the form returns a message confirmation.\n * @param message - The confirmation message (may be a RichText object)\n */\n onMessage?: (message: unknown) => void\n /**\n * Called when the form should redirect after submission.\n * @param redirect - The URL to redirect to\n */\n onRedirect?: (redirect: string) => void\n}\n\n/**\n * Submits a form to the backend API.\n *\n * @param args - Form submission arguments\n * @param args.formId - The ID of the form to submit to\n * @param args.data - The form data to submit\n * @param args.opts - Optional callbacks for handling the response\n * @returns The submission data including confirmation and submissionId\n * @throws Error if the submission fails\n *\n * @example\n * ```tsx\n * const handleSubmit = async (formData: Record<string, unknown>) => {\n * try {\n * const result = await submitForm({\n * formId: '1',\n * data: formData,\n * opts: {\n * onRedirect: (url) => router.push(url),\n * onMessage: (message) => setConfirmation(message),\n * },\n * })\n * console.log('Submission ID:', result.submissionId)\n * } catch (error) {\n * console.error('Form submission failed:', error)\n * }\n * }\n * ```\n */\nexport async function submitForm(args: {\n data: Record<string, unknown>\n formId: number | string\n opts?: SubmitFormOptions\n}) {\n const response = await fetch(apiUrl, {\n body: JSON.stringify({ data: args.data, formId: args.formId }),\n headers: {\n 'Content-Type': 'application/json',\n },\n method: 'POST',\n })\n\n const result: FormSubmissionResponse = await response.json()\n\n if (!result.success) {\n throw new Error(result.error?.message || 'Submission failed')\n }\n\n if (result.data?.confirmation.type === 'redirect' && result.data.confirmation.redirect) {\n args.opts?.onRedirect?.(result.data.confirmation.redirect)\n }\n\n if (result.data?.confirmation.type === 'message') {\n args.opts?.onMessage?.(result.data.confirmation.message)\n }\n\n return result.data\n}\n"],"names":["apiUrl","submitForm","args","response","fetch","body","JSON","stringify","data","formId","headers","method","result","json","success","Error","error","message","confirmation","type","redirect","opts","onRedirect","onMessage"],"mappings":"AAAA,MAAMA,SAAS;AAqCf;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,GACD,OAAO,eAAeC,WAAWC,IAIhC;IACC,MAAMC,WAAW,MAAMC,MAAMJ,QAAQ;QACnCK,MAAMC,KAAKC,SAAS,CAAC;YAAEC,MAAMN,KAAKM,IAAI;YAAEC,QAAQP,KAAKO,MAAM;QAAC;QAC5DC,SAAS;YACP,gBAAgB;QAClB;QACAC,QAAQ;IACV;IAEA,MAAMC,SAAiC,MAAMT,SAASU,IAAI;IAE1D,IAAI,CAACD,OAAOE,OAAO,EAAE;QACnB,MAAM,IAAIC,MAAMH,OAAOI,KAAK,EAAEC,WAAW;IAC3C;IAEA,IAAIL,OAAOJ,IAAI,EAAEU,aAAaC,SAAS,cAAcP,OAAOJ,IAAI,CAACU,YAAY,CAACE,QAAQ,EAAE;QACtFlB,KAAKmB,IAAI,EAAEC,aAAaV,OAAOJ,IAAI,CAACU,YAAY,CAACE,QAAQ;IAC3D;IAEA,IAAIR,OAAOJ,IAAI,EAAEU,aAAaC,SAAS,WAAW;QAChDjB,KAAKmB,IAAI,EAAEE,YAAYX,OAAOJ,IAAI,CAACU,YAAY,CAACD,OAAO;IACzD;IAEA,OAAOL,OAAOJ,IAAI;AACpB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mobilizehub/payload-plugin",
3
- "version": "0.5.3",
3
+ "version": "0.6.0",
4
4
  "description": "Edvocacy plugin for Payload",
5
5
  "license": "MIT",
6
6
  "private": false,