@kelpie/ui 0.7.0 → 0.9.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.
Files changed (349) hide show
  1. package/dist/api/resource.d.ts +59 -6
  2. package/dist/api/resource.d.ts.map +1 -1
  3. package/dist/api/resource.js +106 -24
  4. package/dist/api/resource.js.map +1 -1
  5. package/dist/api/resources/companies.d.ts +2 -0
  6. package/dist/api/resources/companies.d.ts.map +1 -1
  7. package/dist/api/resources/companies.js +5 -1
  8. package/dist/api/resources/companies.js.map +1 -1
  9. package/dist/api/resources/consentPurposes.d.ts +13 -0
  10. package/dist/api/resources/consentPurposes.d.ts.map +1 -0
  11. package/dist/api/resources/consentPurposes.js +39 -0
  12. package/dist/api/resources/consentPurposes.js.map +1 -0
  13. package/dist/api/resources/customFields.d.ts +15 -0
  14. package/dist/api/resources/customFields.d.ts.map +1 -0
  15. package/dist/api/resources/customFields.js +41 -0
  16. package/dist/api/resources/customFields.js.map +1 -0
  17. package/dist/api/resources/decisions.d.ts +2 -0
  18. package/dist/api/resources/decisions.d.ts.map +1 -1
  19. package/dist/api/resources/decisions.js +1 -0
  20. package/dist/api/resources/decisions.js.map +1 -1
  21. package/dist/api/resources/emailDomainLinker.d.ts +16 -0
  22. package/dist/api/resources/emailDomainLinker.d.ts.map +1 -0
  23. package/dist/api/resources/emailDomainLinker.js +16 -0
  24. package/dist/api/resources/emailDomainLinker.js.map +1 -0
  25. package/dist/api/resources/enquiries.d.ts +39 -0
  26. package/dist/api/resources/enquiries.d.ts.map +1 -0
  27. package/dist/api/resources/enquiries.js +68 -0
  28. package/dist/api/resources/enquiries.js.map +1 -0
  29. package/dist/api/resources/forms.d.ts +21 -1
  30. package/dist/api/resources/forms.d.ts.map +1 -1
  31. package/dist/api/resources/forms.js +44 -17
  32. package/dist/api/resources/forms.js.map +1 -1
  33. package/dist/api/resources/handbookPages.d.ts +1 -1
  34. package/dist/api/resources/handbookPages.js +1 -1
  35. package/dist/api/resources/importJobs.d.ts +5 -0
  36. package/dist/api/resources/importJobs.d.ts.map +1 -1
  37. package/dist/api/resources/importJobs.js +3 -0
  38. package/dist/api/resources/importJobs.js.map +1 -1
  39. package/dist/api/resources/listMembers.d.ts +27 -0
  40. package/dist/api/resources/listMembers.d.ts.map +1 -0
  41. package/dist/api/resources/listMembers.js +108 -0
  42. package/dist/api/resources/listMembers.js.map +1 -0
  43. package/dist/api/resources/lists.d.ts +14 -0
  44. package/dist/api/resources/lists.d.ts.map +1 -0
  45. package/dist/api/resources/lists.js +34 -0
  46. package/dist/api/resources/lists.js.map +1 -0
  47. package/dist/api/resources/opportunities.d.ts +2 -0
  48. package/dist/api/resources/opportunities.d.ts.map +1 -1
  49. package/dist/api/resources/opportunities.js +1 -0
  50. package/dist/api/resources/opportunities.js.map +1 -1
  51. package/dist/api/resources/people.d.ts.map +1 -1
  52. package/dist/api/resources/people.js +4 -1
  53. package/dist/api/resources/people.js.map +1 -1
  54. package/dist/api/resources/roles.d.ts +2 -0
  55. package/dist/api/resources/roles.d.ts.map +1 -1
  56. package/dist/api/resources/roles.js +1 -1
  57. package/dist/api/resources/roles.js.map +1 -1
  58. package/dist/api/resources/sampleData.d.ts +17 -0
  59. package/dist/api/resources/sampleData.d.ts.map +1 -0
  60. package/dist/api/resources/sampleData.js +31 -0
  61. package/dist/api/resources/sampleData.js.map +1 -0
  62. package/dist/api/resources/webhooks.d.ts.map +1 -1
  63. package/dist/api/resources/webhooks.js +7 -22
  64. package/dist/api/resources/webhooks.js.map +1 -1
  65. package/dist/app/KelpieApp.d.ts.map +1 -1
  66. package/dist/app/KelpieApp.js +9 -1
  67. package/dist/app/KelpieApp.js.map +1 -1
  68. package/dist/components/ActivitiesPanel.d.ts.map +1 -1
  69. package/dist/components/ActivitiesPanel.js +3 -1
  70. package/dist/components/ActivitiesPanel.js.map +1 -1
  71. package/dist/components/ColumnPicker.d.ts +24 -0
  72. package/dist/components/ColumnPicker.d.ts.map +1 -0
  73. package/dist/components/ColumnPicker.js +56 -0
  74. package/dist/components/ColumnPicker.js.map +1 -0
  75. package/dist/components/CustomFieldsPanel.d.ts +23 -0
  76. package/dist/components/CustomFieldsPanel.d.ts.map +1 -0
  77. package/dist/components/CustomFieldsPanel.js +116 -0
  78. package/dist/components/CustomFieldsPanel.js.map +1 -0
  79. package/dist/components/DataTable.d.ts +16 -1
  80. package/dist/components/DataTable.d.ts.map +1 -1
  81. package/dist/components/DataTable.js +51 -10
  82. package/dist/components/DataTable.js.map +1 -1
  83. package/dist/components/DecisionsPanel.d.ts.map +1 -1
  84. package/dist/components/DecisionsPanel.js +4 -3
  85. package/dist/components/DecisionsPanel.js.map +1 -1
  86. package/dist/components/EntitySearch.d.ts.map +1 -1
  87. package/dist/components/EntitySearch.js +11 -5
  88. package/dist/components/EntitySearch.js.map +1 -1
  89. package/dist/components/FormsPanel.d.ts +18 -0
  90. package/dist/components/FormsPanel.d.ts.map +1 -0
  91. package/dist/components/FormsPanel.js +37 -0
  92. package/dist/components/FormsPanel.js.map +1 -0
  93. package/dist/components/ListsPanel.d.ts +14 -0
  94. package/dist/components/ListsPanel.d.ts.map +1 -0
  95. package/dist/components/ListsPanel.js +56 -0
  96. package/dist/components/ListsPanel.js.map +1 -0
  97. package/dist/components/NotesPanel.d.ts.map +1 -1
  98. package/dist/components/NotesPanel.js +6 -5
  99. package/dist/components/NotesPanel.js.map +1 -1
  100. package/dist/components/Paginator.d.ts +26 -0
  101. package/dist/components/Paginator.d.ts.map +1 -0
  102. package/dist/components/Paginator.js +16 -0
  103. package/dist/components/Paginator.js.map +1 -0
  104. package/dist/components/PlanAttention.d.ts +3 -2
  105. package/dist/components/PlanAttention.d.ts.map +1 -1
  106. package/dist/components/PlanAttention.js +7 -3
  107. package/dist/components/PlanAttention.js.map +1 -1
  108. package/dist/components/PlanPanel.d.ts.map +1 -1
  109. package/dist/components/PlanPanel.js +2 -1
  110. package/dist/components/PlanPanel.js.map +1 -1
  111. package/dist/components/Shell.d.ts.map +1 -1
  112. package/dist/components/Shell.js +4 -0
  113. package/dist/components/Shell.js.map +1 -1
  114. package/dist/components/useHasCustomFields.d.ts +13 -0
  115. package/dist/components/useHasCustomFields.d.ts.map +1 -0
  116. package/dist/components/useHasCustomFields.js +16 -0
  117. package/dist/components/useHasCustomFields.js.map +1 -0
  118. package/dist/index.d.ts +8 -2
  119. package/dist/index.d.ts.map +1 -1
  120. package/dist/index.js +4 -1
  121. package/dist/index.js.map +1 -1
  122. package/dist/lib/listView.d.ts +15 -0
  123. package/dist/lib/listView.d.ts.map +1 -0
  124. package/dist/lib/listView.js +66 -0
  125. package/dist/lib/listView.js.map +1 -0
  126. package/dist/lib/sort.d.ts +43 -0
  127. package/dist/lib/sort.d.ts.map +1 -0
  128. package/dist/lib/sort.js +102 -0
  129. package/dist/lib/sort.js.map +1 -0
  130. package/dist/pages/CompaniesPage.d.ts.map +1 -1
  131. package/dist/pages/CompaniesPage.js +88 -3
  132. package/dist/pages/CompaniesPage.js.map +1 -1
  133. package/dist/pages/CompanyDetail.d.ts.map +1 -1
  134. package/dist/pages/CompanyDetail.js +40 -15
  135. package/dist/pages/CompanyDetail.js.map +1 -1
  136. package/dist/pages/DealDetail.d.ts.map +1 -1
  137. package/dist/pages/DealDetail.js +20 -1
  138. package/dist/pages/DealDetail.js.map +1 -1
  139. package/dist/pages/DealsPage.d.ts.map +1 -1
  140. package/dist/pages/DealsPage.js +134 -14
  141. package/dist/pages/DealsPage.js.map +1 -1
  142. package/dist/pages/DecisionsPage.d.ts.map +1 -1
  143. package/dist/pages/DecisionsPage.js +52 -8
  144. package/dist/pages/DecisionsPage.js.map +1 -1
  145. package/dist/pages/EnquiriesPage.d.ts +2 -0
  146. package/dist/pages/EnquiriesPage.d.ts.map +1 -0
  147. package/dist/pages/EnquiriesPage.js +251 -0
  148. package/dist/pages/EnquiriesPage.js.map +1 -0
  149. package/dist/pages/EnquiryDetail.d.ts +2 -0
  150. package/dist/pages/EnquiryDetail.d.ts.map +1 -0
  151. package/dist/pages/EnquiryDetail.js +207 -0
  152. package/dist/pages/EnquiryDetail.js.map +1 -0
  153. package/dist/pages/EnquiryStageSettingsPage.d.ts +3 -0
  154. package/dist/pages/EnquiryStageSettingsPage.d.ts.map +1 -0
  155. package/dist/pages/EnquiryStageSettingsPage.js +34 -0
  156. package/dist/pages/EnquiryStageSettingsPage.js.map +1 -0
  157. package/dist/pages/FormDetail.d.ts.map +1 -1
  158. package/dist/pages/FormDetail.js +19 -1
  159. package/dist/pages/FormDetail.js.map +1 -1
  160. package/dist/pages/FormsPage.d.ts.map +1 -1
  161. package/dist/pages/FormsPage.js +53 -2
  162. package/dist/pages/FormsPage.js.map +1 -1
  163. package/dist/pages/FundraisingPage.d.ts.map +1 -1
  164. package/dist/pages/FundraisingPage.js +116 -14
  165. package/dist/pages/FundraisingPage.js.map +1 -1
  166. package/dist/pages/HiringPage.d.ts.map +1 -1
  167. package/dist/pages/HiringPage.js +27 -4
  168. package/dist/pages/HiringPage.js.map +1 -1
  169. package/dist/pages/ListDetail.d.ts +11 -0
  170. package/dist/pages/ListDetail.d.ts.map +1 -0
  171. package/dist/pages/ListDetail.js +214 -0
  172. package/dist/pages/ListDetail.js.map +1 -0
  173. package/dist/pages/ListsPage.d.ts +9 -0
  174. package/dist/pages/ListsPage.d.ts.map +1 -0
  175. package/dist/pages/ListsPage.js +125 -0
  176. package/dist/pages/ListsPage.js.map +1 -0
  177. package/dist/pages/OpportunitiesPage.d.ts.map +1 -1
  178. package/dist/pages/OpportunitiesPage.js +97 -14
  179. package/dist/pages/OpportunitiesPage.js.map +1 -1
  180. package/dist/pages/OpportunityDetail.d.ts.map +1 -1
  181. package/dist/pages/OpportunityDetail.js +64 -3
  182. package/dist/pages/OpportunityDetail.js.map +1 -1
  183. package/dist/pages/PartnershipDetail.d.ts.map +1 -1
  184. package/dist/pages/PartnershipDetail.js +19 -1
  185. package/dist/pages/PartnershipDetail.js.map +1 -1
  186. package/dist/pages/PartnershipsPage.d.ts.map +1 -1
  187. package/dist/pages/PartnershipsPage.js +111 -14
  188. package/dist/pages/PartnershipsPage.js.map +1 -1
  189. package/dist/pages/PeoplePage.d.ts.map +1 -1
  190. package/dist/pages/PeoplePage.js +119 -3
  191. package/dist/pages/PeoplePage.js.map +1 -1
  192. package/dist/pages/PersonDetail.d.ts.map +1 -1
  193. package/dist/pages/PersonDetail.js +67 -5
  194. package/dist/pages/PersonDetail.js.map +1 -1
  195. package/dist/pages/PlanningPage.d.ts.map +1 -1
  196. package/dist/pages/PlanningPage.js +88 -17
  197. package/dist/pages/PlanningPage.js.map +1 -1
  198. package/dist/pages/RaiseDetail.d.ts.map +1 -1
  199. package/dist/pages/RaiseDetail.js +13 -1
  200. package/dist/pages/RaiseDetail.js.map +1 -1
  201. package/dist/pages/RoleDetail.d.ts.map +1 -1
  202. package/dist/pages/RoleDetail.js +5 -4
  203. package/dist/pages/RoleDetail.js.map +1 -1
  204. package/dist/pages/SearchPage.d.ts.map +1 -1
  205. package/dist/pages/SearchPage.js +1 -0
  206. package/dist/pages/SearchPage.js.map +1 -1
  207. package/dist/pages/admin/DataPage.d.ts.map +1 -1
  208. package/dist/pages/admin/DataPage.js +126 -13
  209. package/dist/pages/admin/DataPage.js.map +1 -1
  210. package/dist/pages/admin/FieldsPage.d.ts +2 -0
  211. package/dist/pages/admin/FieldsPage.d.ts.map +1 -0
  212. package/dist/pages/admin/FieldsPage.js +164 -0
  213. package/dist/pages/admin/FieldsPage.js.map +1 -0
  214. package/dist/pages/admin/PrivacyPage.d.ts +9 -0
  215. package/dist/pages/admin/PrivacyPage.d.ts.map +1 -0
  216. package/dist/pages/admin/PrivacyPage.js +170 -0
  217. package/dist/pages/admin/PrivacyPage.js.map +1 -0
  218. package/dist/pages/admin/WebhookDeliveries.d.ts.map +1 -1
  219. package/dist/pages/admin/WebhookDeliveries.js +3 -2
  220. package/dist/pages/admin/WebhookDeliveries.js.map +1 -1
  221. package/dist/pages/admin/WebhooksPage.d.ts.map +1 -1
  222. package/dist/pages/admin/WebhooksPage.js +3 -2
  223. package/dist/pages/admin/WebhooksPage.js.map +1 -1
  224. package/dist/pages/admin/WorkspaceSettingsPage.d.ts.map +1 -1
  225. package/dist/pages/admin/WorkspaceSettingsPage.js +45 -2
  226. package/dist/pages/admin/WorkspaceSettingsPage.js.map +1 -1
  227. package/dist/pages/dashboard/attention.d.ts.map +1 -1
  228. package/dist/pages/dashboard/attention.js +3 -0
  229. package/dist/pages/dashboard/attention.js.map +1 -1
  230. package/dist/pages/forms/AddFieldMenu.d.ts +17 -0
  231. package/dist/pages/forms/AddFieldMenu.d.ts.map +1 -0
  232. package/dist/pages/forms/AddFieldMenu.js +43 -0
  233. package/dist/pages/forms/AddFieldMenu.js.map +1 -0
  234. package/dist/pages/forms/EmbedPanel.d.ts +4 -0
  235. package/dist/pages/forms/EmbedPanel.d.ts.map +1 -1
  236. package/dist/pages/forms/EmbedPanel.js +62 -3
  237. package/dist/pages/forms/EmbedPanel.js.map +1 -1
  238. package/dist/pages/forms/FieldsEditor.d.ts.map +1 -1
  239. package/dist/pages/forms/FieldsEditor.js +179 -36
  240. package/dist/pages/forms/FieldsEditor.js.map +1 -1
  241. package/dist/pages/forms/FormActions.d.ts +6 -0
  242. package/dist/pages/forms/FormActions.d.ts.map +1 -0
  243. package/dist/pages/forms/FormActions.js +149 -0
  244. package/dist/pages/forms/FormActions.js.map +1 -0
  245. package/dist/pages/forms/FormSettings.d.ts.map +1 -1
  246. package/dist/pages/forms/FormSettings.js +15 -19
  247. package/dist/pages/forms/FormSettings.js.map +1 -1
  248. package/dist/pages/forms/SubmissionDetail.d.ts +2 -0
  249. package/dist/pages/forms/SubmissionDetail.d.ts.map +1 -0
  250. package/dist/pages/forms/SubmissionDetail.js +76 -0
  251. package/dist/pages/forms/SubmissionDetail.js.map +1 -0
  252. package/dist/pages/forms/SubmissionsTable.d.ts.map +1 -1
  253. package/dist/pages/forms/SubmissionsTable.js +57 -8
  254. package/dist/pages/forms/SubmissionsTable.js.map +1 -1
  255. package/dist/pages/forms/fieldList.d.ts +11 -5
  256. package/dist/pages/forms/fieldList.d.ts.map +1 -1
  257. package/dist/pages/forms/fieldList.js +66 -6
  258. package/dist/pages/forms/fieldList.js.map +1 -1
  259. package/dist/pages/forms/template.d.ts +17 -0
  260. package/dist/pages/forms/template.d.ts.map +1 -1
  261. package/dist/pages/forms/template.js +93 -0
  262. package/dist/pages/forms/template.js.map +1 -1
  263. package/dist/pages/handbook/HandbookPage.js +1 -1
  264. package/dist/pages/hiringDirectory.js +2 -2
  265. package/dist/pages/onboarding/WorkspaceStepPage.d.ts.map +1 -1
  266. package/dist/pages/onboarding/WorkspaceStepPage.js +15 -2
  267. package/dist/pages/onboarding/WorkspaceStepPage.js.map +1 -1
  268. package/dist/pages/positionDirectory.js +2 -2
  269. package/package.json +2 -2
  270. package/src/api/resource.ts +155 -32
  271. package/src/api/resources/companies.ts +7 -1
  272. package/src/api/resources/consentPurposes.ts +83 -0
  273. package/src/api/resources/customFields.ts +88 -0
  274. package/src/api/resources/decisions.ts +3 -0
  275. package/src/api/resources/emailDomainLinker.ts +43 -0
  276. package/src/api/resources/enquiries.ts +118 -0
  277. package/src/api/resources/forms.ts +59 -19
  278. package/src/api/resources/handbookPages.ts +1 -1
  279. package/src/api/resources/importJobs.ts +9 -0
  280. package/src/api/resources/listMembers.ts +173 -0
  281. package/src/api/resources/lists.ts +61 -0
  282. package/src/api/resources/opportunities.ts +3 -0
  283. package/src/api/resources/people.ts +4 -1
  284. package/src/api/resources/roles.ts +3 -1
  285. package/src/api/resources/sampleData.ts +57 -0
  286. package/src/api/resources/webhooks.ts +8 -24
  287. package/src/app/KelpieApp.tsx +16 -0
  288. package/src/components/ActivitiesPanel.tsx +3 -10
  289. package/src/components/ColumnPicker.tsx +124 -0
  290. package/src/components/CustomFieldsPanel.tsx +330 -0
  291. package/src/components/DataTable.tsx +89 -10
  292. package/src/components/DecisionsPanel.tsx +4 -12
  293. package/src/components/EntitySearch.tsx +12 -5
  294. package/src/components/FormsPanel.tsx +86 -0
  295. package/src/components/ListsPanel.tsx +176 -0
  296. package/src/components/NotesPanel.tsx +6 -14
  297. package/src/components/Paginator.tsx +78 -0
  298. package/src/components/PlanAttention.tsx +20 -3
  299. package/src/components/PlanPanel.tsx +2 -10
  300. package/src/components/Shell.tsx +4 -0
  301. package/src/components/useHasCustomFields.ts +18 -0
  302. package/src/index.ts +10 -1
  303. package/src/lib/listView.ts +149 -0
  304. package/src/lib/sort.ts +143 -0
  305. package/src/pages/CompaniesPage.tsx +125 -11
  306. package/src/pages/CompanyDetail.tsx +98 -63
  307. package/src/pages/DealDetail.tsx +34 -0
  308. package/src/pages/DealsPage.tsx +178 -24
  309. package/src/pages/DecisionsPage.tsx +75 -18
  310. package/src/pages/EnquiriesPage.tsx +434 -0
  311. package/src/pages/EnquiryDetail.tsx +554 -0
  312. package/src/pages/EnquiryStageSettingsPage.tsx +63 -0
  313. package/src/pages/FormDetail.tsx +23 -1
  314. package/src/pages/FormsPage.tsx +81 -17
  315. package/src/pages/FundraisingPage.tsx +152 -24
  316. package/src/pages/HiringPage.tsx +39 -11
  317. package/src/pages/ListDetail.tsx +470 -0
  318. package/src/pages/ListsPage.tsx +281 -0
  319. package/src/pages/OpportunitiesPage.tsx +130 -24
  320. package/src/pages/OpportunityDetail.tsx +168 -2
  321. package/src/pages/PartnershipDetail.tsx +35 -0
  322. package/src/pages/PartnershipsPage.tsx +149 -24
  323. package/src/pages/PeoplePage.tsx +157 -11
  324. package/src/pages/PersonDetail.tsx +157 -6
  325. package/src/pages/PlanningPage.tsx +259 -29
  326. package/src/pages/RaiseDetail.tsx +24 -0
  327. package/src/pages/RoleDetail.tsx +21 -26
  328. package/src/pages/SearchPage.tsx +1 -0
  329. package/src/pages/admin/DataPage.tsx +324 -13
  330. package/src/pages/admin/FieldsPage.tsx +455 -0
  331. package/src/pages/admin/PrivacyPage.tsx +461 -0
  332. package/src/pages/admin/WebhookDeliveries.tsx +10 -15
  333. package/src/pages/admin/WebhooksPage.tsx +14 -19
  334. package/src/pages/admin/WorkspaceSettingsPage.tsx +134 -2
  335. package/src/pages/dashboard/attention.ts +3 -0
  336. package/src/pages/forms/AddFieldMenu.tsx +121 -0
  337. package/src/pages/forms/EmbedPanel.tsx +163 -2
  338. package/src/pages/forms/FieldsEditor.tsx +539 -165
  339. package/src/pages/forms/FormActions.tsx +552 -0
  340. package/src/pages/forms/FormSettings.tsx +29 -70
  341. package/src/pages/forms/SubmissionDetail.tsx +249 -0
  342. package/src/pages/forms/SubmissionsTable.tsx +112 -26
  343. package/src/pages/forms/fieldList.ts +68 -5
  344. package/src/pages/forms/template.ts +103 -0
  345. package/src/pages/handbook/HandbookPage.tsx +1 -1
  346. package/src/pages/hiringDirectory.ts +2 -2
  347. package/src/pages/onboarding/WorkspaceStepPage.tsx +31 -3
  348. package/src/pages/positionDirectory.ts +2 -2
  349. package/src/styles.css +3 -3
@@ -0,0 +1,249 @@
1
+ import type { Form, FormField, FormSubmission, FormSubmissionActionEntry } from '@kelpie/schemas'
2
+ import { useMemo } from 'react'
3
+ import { Link, useParams } from 'react-router'
4
+
5
+ import { useTimezone } from '../../api/resources/account.ts'
6
+ import { useCompanies } from '../../api/resources/companies.ts'
7
+ import { useForm, useFormSubmission } from '../../api/resources/forms.ts'
8
+ import { usePeople } from '../../api/resources/people.ts'
9
+ import { ErrorPanel, LoadingPanel, NotFoundPanel } from '../../components/QueryState.tsx'
10
+ import { SectionHeader } from '../../components/SectionHeader.tsx'
11
+ import { formatDateTime } from '../../lib/dates.ts'
12
+
13
+ /**
14
+ * One submission: every answer, the records it linked, and the action log.
15
+ *
16
+ * Nested under the form so the field labels (which live on the form, not the
17
+ * submission) are at hand, and so "back" returns to that form's Submissions tab
18
+ * rather than the forms list.
19
+ */
20
+
21
+ /** `api.md`: `?limit=` maxes out at 200. */
22
+ const MAX_PAGE = 200
23
+
24
+ export function SubmissionDetail(): React.JSX.Element {
25
+ const { id: formId, submissionId } = useParams()
26
+ const form = useForm(formId)
27
+ const submission = useFormSubmission(formId, submissionId)
28
+ const people = usePeople({ limit: MAX_PAGE })
29
+ const companies = useCompanies({ limit: MAX_PAGE })
30
+ const timezone = useTimezone()
31
+
32
+ const nameById = useMemo(
33
+ () => new Map([...people.records, ...companies.records].map((record) => [record.id, record.name])),
34
+ [people.records, companies.records],
35
+ )
36
+
37
+ if (form.isNotFound || submission.isNotFound) {
38
+ return (
39
+ <NotFoundPanel
40
+ label="Submission"
41
+ backTo={formId === undefined ? '/forms' : `/forms/${formId}`}
42
+ />
43
+ )
44
+ }
45
+
46
+ if (form.error !== null) {
47
+ return <ErrorPanel error={form.error} />
48
+ }
49
+
50
+ if (submission.error !== null) {
51
+ return <ErrorPanel error={submission.error} />
52
+ }
53
+
54
+ if (
55
+ form.isLoading ||
56
+ submission.isLoading ||
57
+ form.record === undefined ||
58
+ submission.record === undefined
59
+ ) {
60
+ return <LoadingPanel label="Loading submission…" />
61
+ }
62
+
63
+ return (
64
+ <SubmissionDetailView
65
+ form={form.record}
66
+ submission={submission.record}
67
+ nameById={nameById}
68
+ timezone={timezone}
69
+ />
70
+ )
71
+ }
72
+
73
+ function SubmissionDetailView({
74
+ form,
75
+ submission,
76
+ nameById,
77
+ timezone,
78
+ }: {
79
+ readonly form: Form
80
+ readonly submission: FormSubmission
81
+ readonly nameById: ReadonlyMap<string, string>
82
+ readonly timezone: string
83
+ }): React.JSX.Element {
84
+ const answers = form.fields.map((field) => ({
85
+ field,
86
+ value: displayAnswer(field, submission.answers[field.id]),
87
+ }))
88
+
89
+ return (
90
+ <div className="animate-fade-in mx-auto max-w-2xl">
91
+ <Link
92
+ to={`/forms/${form.id}`}
93
+ className="mb-4 inline-flex text-[12px] font-medium text-ink-muted transition hover:text-accent"
94
+ >
95
+ ← {form.name}
96
+ </Link>
97
+
98
+ <SectionHeader
99
+ title="Submission"
100
+ description={`Received ${formatDateTime(submission.submittedAt, timezone)}.`}
101
+ />
102
+
103
+ <div className="mt-6 space-y-6">
104
+ <section>
105
+ <h3 className="mb-2 text-[11px] font-semibold uppercase tracking-wide text-ink-faint">
106
+ Answers
107
+ </h3>
108
+ <dl className="divide-y divide-border rounded-md border border-border">
109
+ {answers.map(({ field, value }) => (
110
+ <div
111
+ key={field.id}
112
+ className="grid gap-0.5 px-3 py-2.5 sm:grid-cols-[10rem_1fr] sm:gap-3"
113
+ >
114
+ <dt className="text-[12px] font-medium text-ink-muted">
115
+ {field.label}
116
+ {field.required && <span className="ml-0.5 text-danger">*</span>}
117
+ </dt>
118
+ <dd className="whitespace-pre-wrap text-[13px] text-ink">
119
+ {value === null ? <span className="text-ink-faint">—</span> : value}
120
+ </dd>
121
+ </div>
122
+ ))}
123
+ </dl>
124
+ </section>
125
+
126
+ <section>
127
+ <h3 className="mb-2 text-[11px] font-semibold uppercase tracking-wide text-ink-faint">
128
+ Records
129
+ </h3>
130
+ <ul className="space-y-1.5 rounded-md border border-border px-3 py-2.5 text-[13px]">
131
+ <RecordRow
132
+ label="Person"
133
+ to={submission.personId === null ? null : `/people/${submission.personId}`}
134
+ text={
135
+ submission.personId === null
136
+ ? null
137
+ : (nameById.get(submission.personId) ?? 'Person')
138
+ }
139
+ />
140
+ <RecordRow
141
+ label="Company"
142
+ to={submission.companyId === null ? null : `/companies/${submission.companyId}`}
143
+ text={
144
+ submission.companyId === null
145
+ ? null
146
+ : (nameById.get(submission.companyId) ?? 'Company')
147
+ }
148
+ />
149
+ <RecordRow
150
+ label="Deal"
151
+ to={submission.dealId === null ? null : `/deals/${submission.dealId}`}
152
+ text={submission.dealId === null ? null : 'Open deal'}
153
+ />
154
+ <RecordRow
155
+ label="Opportunity"
156
+ to={
157
+ submission.opportunityId === null
158
+ ? null
159
+ : `/opportunities/${submission.opportunityId}`
160
+ }
161
+ text={submission.opportunityId === null ? null : 'Open opportunity'}
162
+ />
163
+ <RecordRow
164
+ label="Partnership"
165
+ to={
166
+ submission.partnershipId === null
167
+ ? null
168
+ : `/partnerships/${submission.partnershipId}`
169
+ }
170
+ text={submission.partnershipId === null ? null : 'Open partnership'}
171
+ />
172
+ <RecordRow
173
+ label="Enquiry"
174
+ to={submission.enquiryId === null ? null : `/enquiries/${submission.enquiryId}`}
175
+ text={submission.enquiryId === null ? null : 'Open enquiry'}
176
+ />
177
+ </ul>
178
+ </section>
179
+
180
+ {submission.actionLog.length > 0 && (
181
+ <section>
182
+ <h3 className="mb-2 text-[11px] font-semibold uppercase tracking-wide text-ink-faint">
183
+ Action log
184
+ </h3>
185
+ <ul className="divide-y divide-border rounded-md border border-border">
186
+ {submission.actionLog.map((entry, index) => (
187
+ <li key={`${entry.action}-${String(index)}`} className="px-3 py-2.5">
188
+ <div className="flex items-baseline justify-between gap-2">
189
+ <span className="font-mono text-[12px] text-ink">{entry.action}</span>
190
+ <ActionStatus status={entry.status} />
191
+ </div>
192
+ {entry.detail.length > 0 && (
193
+ <p className="mt-1 text-[12px] text-ink-muted">{entry.detail}</p>
194
+ )}
195
+ </li>
196
+ ))}
197
+ </ul>
198
+ </section>
199
+ )}
200
+ </div>
201
+ </div>
202
+ )
203
+ }
204
+
205
+ function RecordRow({
206
+ label,
207
+ to,
208
+ text,
209
+ }: {
210
+ readonly label: string
211
+ readonly to: string | null
212
+ readonly text: string | null
213
+ }): React.JSX.Element {
214
+ return (
215
+ <li className="flex items-baseline gap-2 py-0.5">
216
+ <span className="w-28 shrink-0 text-[12px] font-medium text-ink-muted">{label}</span>
217
+ {to === null || text === null ? (
218
+ <span className="text-ink-faint">—</span>
219
+ ) : (
220
+ <Link to={to} className="text-accent hover:underline">
221
+ {text}
222
+ </Link>
223
+ )}
224
+ </li>
225
+ )
226
+ }
227
+
228
+ function ActionStatus({
229
+ status,
230
+ }: {
231
+ readonly status: FormSubmissionActionEntry['status']
232
+ }): React.JSX.Element {
233
+ const className =
234
+ status === 'ok' ? 'text-success' : status === 'error' ? 'text-danger' : 'text-ink-faint'
235
+
236
+ return <span className={`text-[11px] font-medium uppercase ${className}`}>{status}</span>
237
+ }
238
+
239
+ function displayAnswer(field: FormField, answer: string | undefined): string | null {
240
+ if (answer === undefined || answer.trim().length === 0) {
241
+ return null
242
+ }
243
+
244
+ if (field.type === 'select') {
245
+ return field.options.find((option) => option.key === answer)?.value ?? answer
246
+ }
247
+
248
+ return answer
249
+ }
@@ -1,6 +1,6 @@
1
- import type { Form, FormSubmission } from '@kelpie/schemas'
1
+ import type { Form, FormSubmission, FormSubmissionActionEntry } from '@kelpie/schemas'
2
2
  import { useMemo } from 'react'
3
- import { Link } from 'react-router'
3
+ import { Link, useNavigate } from 'react-router'
4
4
 
5
5
  import type { RecordListResult } from '../../api/resource.ts'
6
6
  import { useTimezone } from '../../api/resources/account.ts'
@@ -8,6 +8,7 @@ import { useCompanies } from '../../api/resources/companies.ts'
8
8
  import { usePeople } from '../../api/resources/people.ts'
9
9
  import { DataTable } from '../../components/DataTable.tsx'
10
10
  import type { Column } from '../../components/DataTable.tsx'
11
+ import { Paginator } from '../../components/Paginator.tsx'
11
12
  import { ErrorPanel, LoadingPanel } from '../../components/QueryState.tsx'
12
13
  import { SectionHeader } from '../../components/SectionHeader.tsx'
13
14
  import { formatDateTime } from '../../lib/dates.ts'
@@ -19,6 +20,10 @@ import { formatDateTime } from '../../lib/dates.ts'
19
20
  * same way the Decisions page does it: `api.md` has no include-expansion, and
20
21
  * neither list takes a set of bare ids. A record past those pages is still
21
22
  * linked, by its type rather than its name, which beats a raw id.
23
+ *
24
+ * A row opens `/forms/:id/submissions/:submissionId`. Inline person/company
25
+ * links still navigate, and stop the row click so opening a person does not
26
+ * also open the submission.
22
27
  */
23
28
 
24
29
  /** `api.md`: `?limit=` maxes out at 200. */
@@ -30,6 +35,7 @@ export interface SubmissionsTableProps {
30
35
  }
31
36
 
32
37
  export function SubmissionsTable({ form, submissions }: SubmissionsTableProps): React.JSX.Element {
38
+ const navigate = useNavigate()
33
39
  const people = usePeople({ limit: MAX_PAGE })
34
40
  const companies = useCompanies({ limit: MAX_PAGE })
35
41
  const timezone = useTimezone()
@@ -65,17 +71,33 @@ export function SubmissionsTable({ form, submissions }: SubmissionsTableProps):
65
71
  ),
66
72
  },
67
73
  {
68
- key: 'deal',
69
- header: 'Deal',
70
- className: 'w-24',
71
- render: (submission) =>
72
- submission.dealId === null ? (
73
- <span className="text-ink-faint">—</span>
74
- ) : (
75
- <Link to={`/deals/${submission.dealId}`} className="text-accent hover:underline">
76
- View
77
- </Link>
78
- ),
74
+ key: 'created',
75
+ header: 'Created',
76
+ className: 'w-40',
77
+ render: (submission) => (
78
+ <div className="flex flex-wrap gap-2 text-[12px]">
79
+ <CreatedLink to={`/deals/${submission.dealId ?? ''}`} id={submission.dealId} label="deal" />
80
+ <CreatedLink
81
+ to={`/opportunities/${submission.opportunityId ?? ''}`}
82
+ id={submission.opportunityId}
83
+ label="opp"
84
+ />
85
+ <CreatedLink
86
+ to={`/partnerships/${submission.partnershipId ?? ''}`}
87
+ id={submission.partnershipId}
88
+ label="prt"
89
+ />
90
+ {submission.dealId === null &&
91
+ submission.opportunityId === null &&
92
+ submission.partnershipId === null && <span className="text-ink-faint">—</span>}
93
+ </div>
94
+ ),
95
+ },
96
+ {
97
+ key: 'actions',
98
+ header: 'Actions',
99
+ className: 'w-32',
100
+ render: (submission) => <ActionLogChip entries={submission.actionLog} />,
79
101
  },
80
102
  {
81
103
  key: 'answers',
@@ -98,28 +120,74 @@ export function SubmissionsTable({ form, submissions }: SubmissionsTableProps):
98
120
  <div>
99
121
  <SectionHeader
100
122
  title="Submissions"
101
- description="Inbound answers, and the records each one created or matched."
123
+ description="Inbound answers, and the records each one created or matched. Click a row to open it."
102
124
  />
125
+ <Paginator list={submissions} placement="top" />
103
126
  <DataTable
104
127
  columns={columns}
105
128
  rows={submissions.records}
106
129
  getRowId={(submission) => submission.id}
107
130
  emptyMessage="No submissions yet"
131
+ onRowClick={(submission) => {
132
+ void navigate(`/forms/${form.id}/submissions/${submission.id}`)
133
+ }}
108
134
  />
109
- {submissions.hasMore && (
110
- <button
111
- type="button"
112
- onClick={submissions.loadMore}
113
- disabled={submissions.isLoadingMore}
114
- className="mt-3 rounded-md border border-border px-3 py-1.5 text-[12px] font-medium text-ink transition hover:border-border-strong hover:bg-surface-sunken disabled:opacity-50"
115
- >
116
- {submissions.isLoadingMore ? 'Loading…' : 'Load more'}
117
- </button>
118
- )}
135
+ <Paginator list={submissions} />
119
136
  </div>
120
137
  )
121
138
  }
122
139
 
140
+ function CreatedLink({
141
+ to,
142
+ id,
143
+ label,
144
+ }: {
145
+ readonly to: string
146
+ readonly id: string | null
147
+ readonly label: string
148
+ }): React.JSX.Element | null {
149
+ if (id === null) {
150
+ return null
151
+ }
152
+
153
+ return (
154
+ <Link
155
+ to={to}
156
+ className="text-accent hover:underline"
157
+ onClick={(event) => {
158
+ event.stopPropagation()
159
+ }}
160
+ >
161
+ {label}
162
+ </Link>
163
+ )
164
+ }
165
+
166
+ function ActionLogChip({
167
+ entries,
168
+ }: {
169
+ readonly entries: readonly FormSubmissionActionEntry[]
170
+ }): React.JSX.Element {
171
+ if (entries.length === 0) {
172
+ return <span className="text-ink-faint">—</span>
173
+ }
174
+
175
+ const errors = entries.filter((entry) => entry.status === 'error').length
176
+ const skipped = entries.filter((entry) => entry.status === 'skipped').length
177
+ const ok = entries.length - errors - skipped
178
+ const detail = entries
179
+ .map((entry) => `${entry.action}: ${entry.status}${entry.detail.length > 0 ? ` — ${entry.detail}` : ''}`)
180
+ .join('\n')
181
+
182
+ return (
183
+ <span title={detail} className="inline-flex gap-1 font-mono text-[11px] text-ink-muted">
184
+ {ok > 0 && <span className="text-success">✓{ok}</span>}
185
+ {skipped > 0 && <span className="text-ink-faint">−{skipped}</span>}
186
+ {errors > 0 && <span className="text-danger">!{errors}</span>}
187
+ </span>
188
+ )
189
+ }
190
+
123
191
  function RecordLink({
124
192
  to,
125
193
  id,
@@ -136,12 +204,30 @@ function RecordLink({
136
204
  }
137
205
 
138
206
  return (
139
- <Link to={`${to}/${id}`} className="text-accent hover:underline">
207
+ <Link
208
+ to={`${to}/${id}`}
209
+ className="text-accent hover:underline"
210
+ onClick={(event) => {
211
+ event.stopPropagation()
212
+ }}
213
+ >
140
214
  {name.get(id) ?? fallback}
141
215
  </Link>
142
216
  )
143
217
  }
144
218
 
219
+ /**
220
+ * Every target whose answer the row already shows in its own column: the Person
221
+ * link. A form asks for a name as one box or as a first and last pair, and
222
+ * either way the answers are what the Person column is already displaying.
223
+ */
224
+ const PERSON_COLUMN_TARGETS: ReadonlySet<string> = new Set([
225
+ 'person.name',
226
+ 'person.first_name',
227
+ 'person.last_name',
228
+ 'person.email',
229
+ ])
230
+
145
231
  /**
146
232
  * The first two answers that are not already a column.
147
233
  *
@@ -151,7 +237,7 @@ function RecordLink({
151
237
  */
152
238
  function summarise(form: Form, submission: FormSubmission): string {
153
239
  const parts = form.fields
154
- .filter((field) => field.mapTo !== 'person.name' && field.mapTo !== 'person.email')
240
+ .filter((field) => !PERSON_COLUMN_TARGETS.has(field.mapTo))
155
241
  .map((field) => {
156
242
  const answer = submission.answers[field.id]
157
243
 
@@ -1,11 +1,12 @@
1
1
  import {
2
2
  FORM_FIELD_MAP_TARGETS,
3
3
  FORM_FIELD_TYPES,
4
+ PERSON_CONSENT_TARGET,
4
5
  PERSON_EMAIL_TARGET,
5
6
  } from '@kelpie/schemas'
6
7
  import type { Form, FormFieldInput, FormFieldMapTarget, FormFieldType } from '@kelpie/schemas'
7
8
 
8
- import { NEW_SELECT_OPTIONS } from './template.ts'
9
+ import { CRM_FIELD_PRESETS, NEW_SELECT_OPTIONS } from './template.ts'
9
10
 
10
11
  /**
11
12
  * Editing a form's field list, as pure functions over an array.
@@ -42,6 +43,9 @@ export function toEditableFields(form: Form): EditableField[] {
42
43
  mapTo: field.mapTo,
43
44
  options: field.options.map((option) => ({ ...option })),
44
45
  placeholder: field.placeholder,
46
+ statement: field.statement,
47
+ consentPurposeIds: [...field.consentPurposeIds],
48
+ consentPurposeLabels: { ...field.consentPurposeLabels },
45
49
  }))
46
50
  }
47
51
 
@@ -84,9 +88,12 @@ export function editField(
84
88
  return fields.map((field) => {
85
89
  if (field.id !== id) {
86
90
  // Only one field may carry a CRM target, so taking one releases it
87
- // wherever it was. `submission` is the target that may repeat.
91
+ // wherever it was. `submission` and `person.consent` are the targets
92
+ // that may repeat — consent's uniqueness is per purpose, not per
93
+ // target, and is enforced when the purpose is picked.
88
94
  return change.mapTo !== undefined &&
89
95
  change.mapTo !== 'submission' &&
96
+ change.mapTo !== PERSON_CONSENT_TARGET &&
90
97
  field.mapTo === change.mapTo
91
98
  ? { ...field, mapTo: 'submission' }
92
99
  : field
@@ -94,6 +101,19 @@ export function editField(
94
101
 
95
102
  const next: EditableField = { ...field, ...change }
96
103
 
104
+ // A consent or notice field must map to person.consent; anything else
105
+ // clears any lingering purpose list.
106
+ const isConsentLike = next.type === 'consent' || next.type === 'notice'
107
+ if (isConsentLike && next.mapTo !== PERSON_CONSENT_TARGET) {
108
+ return { ...withoutOptions(next), mapTo: PERSON_CONSENT_TARGET }
109
+ }
110
+ if (next.mapTo === PERSON_CONSENT_TARGET && !isConsentLike) {
111
+ return { ...withoutOptions(next), type: 'consent' }
112
+ }
113
+ if (!isConsentLike && (next.consentPurposeIds ?? []).length > 0) {
114
+ return withoutOptions({ ...next, consentPurposeIds: [] })
115
+ }
116
+
97
117
  if (next.type !== 'select') {
98
118
  return withoutOptions(next)
99
119
  }
@@ -166,6 +186,7 @@ export function findProblems(
166
186
  const list: string[] = []
167
187
  const byField = new Map<string, string>()
168
188
  const seen = new Set<FormFieldMapTarget>()
189
+ const usedConsentPurposes = new Set<string>()
169
190
 
170
191
  if (!fields.some((field) => field.mapTo === PERSON_EMAIL_TARGET)) {
171
192
  list.push(`One field must map to ${PERSON_EMAIL_TARGET}, or no submission can be processed.`)
@@ -176,7 +197,34 @@ export function findProblems(
176
197
  }
177
198
 
178
199
  for (const field of fields) {
179
- if (field.mapTo !== 'submission') {
200
+ if (field.mapTo === PERSON_CONSENT_TARGET) {
201
+ const purposes = field.consentPurposeIds ?? []
202
+ if (field.type === 'notice' && (field.statement ?? '').trim().length === 0) {
203
+ byField.set(field.id, 'A notice field needs a statement.')
204
+ }
205
+ if (purposes.length === 0) {
206
+ byField.set(
207
+ field.id,
208
+ field.type === 'notice'
209
+ ? 'Pick at least one purpose this notice grants implicitly.'
210
+ : 'Pick at least one consent purpose this field offers.',
211
+ )
212
+ } else {
213
+ const localSeen = new Set<string>()
214
+ for (const purposeId of purposes) {
215
+ if (localSeen.has(purposeId)) {
216
+ byField.set(field.id, 'This field lists a purpose twice.')
217
+ break
218
+ }
219
+ localSeen.add(purposeId)
220
+ if (usedConsentPurposes.has(purposeId)) {
221
+ byField.set(field.id, 'Another consent field already offers this purpose.')
222
+ } else {
223
+ usedConsentPurposes.add(purposeId)
224
+ }
225
+ }
226
+ }
227
+ } else if (field.mapTo !== 'submission') {
180
228
  if (seen.has(field.mapTo)) {
181
229
  byField.set(field.id, `Another field already maps to ${field.mapTo}.`)
182
230
  }
@@ -203,7 +251,22 @@ export function isUsable(problems: FieldListProblems): boolean {
203
251
  return problems.list.length === 0 && problems.byField.size === 0
204
252
  }
205
253
 
206
- /** The types a mapping can sensibly render as. Choosing `person.email` implies an email input. */
254
+ /** The types a mapping can sensibly render as. */
207
255
  export function typeForTarget(target: FormFieldMapTarget, current: FormFieldType): FormFieldType {
208
- return target === PERSON_EMAIL_TARGET ? 'email' : current
256
+ if (target === PERSON_EMAIL_TARGET) return 'email'
257
+ if (target === PERSON_CONSENT_TARGET) {
258
+ return current === 'notice' ? 'notice' : 'consent'
259
+ }
260
+ return current === 'consent' || current === 'notice' ? 'text' : current
261
+ }
262
+
263
+ /**
264
+ * The CRM presets whose mapping no current field carries: what "Add field" may
265
+ * still offer. A CRM target may appear once, so a preset leaves the menu the
266
+ * moment a field takes its target and returns when that field releases it.
267
+ */
268
+ export function unusedCrmPresets(fields: readonly EditableField[]): readonly FormFieldInput[] {
269
+ const used = new Set(fields.map((field) => field.mapTo))
270
+
271
+ return CRM_FIELD_PRESETS.filter((preset) => !used.has(preset.mapTo))
209
272
  }
@@ -35,3 +35,106 @@ export const NEW_SELECT_OPTIONS = [
35
35
  { key: 'option_a', value: 'Option A', valueType: 'string' as const },
36
36
  { key: 'option_b', value: 'Option B', valueType: 'string' as const },
37
37
  ]
38
+
39
+ /**
40
+ * One ready-made field per CRM mapping target, in the target enum's order.
41
+ *
42
+ * "Add field" offers these so adding the company field is one click, not
43
+ * add-then-configure. Only one field may carry a CRM target, so the menu shows
44
+ * each preset only while its target is free — `unusedCrmPresets` in
45
+ * `fieldList.ts` does that filtering.
46
+ */
47
+ export const CRM_FIELD_PRESETS: readonly FormFieldInput[] = [
48
+ { label: 'Name', type: 'text', required: false, mapTo: 'person.name', placeholder: 'Your name' },
49
+ // The other way to ask for a name. A form takes one box or this pair; the
50
+ // menu hides whichever is already mapped, so both are offered and neither
51
+ // ends up used twice.
52
+ {
53
+ label: 'First name',
54
+ type: 'text',
55
+ required: false,
56
+ mapTo: 'person.first_name',
57
+ placeholder: 'First name',
58
+ },
59
+ {
60
+ label: 'Last name',
61
+ type: 'text',
62
+ required: false,
63
+ mapTo: 'person.last_name',
64
+ placeholder: 'Last name',
65
+ },
66
+ {
67
+ label: 'Email',
68
+ type: 'email',
69
+ required: true,
70
+ mapTo: 'person.email',
71
+ placeholder: 'you@company.com',
72
+ },
73
+ { label: 'Company', type: 'text', required: false, mapTo: 'company.name', placeholder: 'Company name' },
74
+ {
75
+ label: 'Company website',
76
+ type: 'text',
77
+ required: false,
78
+ mapTo: 'company.domain',
79
+ placeholder: 'company.com',
80
+ },
81
+ { label: 'Job title', type: 'text', required: false, mapTo: 'position.title', placeholder: 'Your role' },
82
+ { label: 'Enquiry name', type: 'text', required: false, mapTo: 'enquiry.name', placeholder: null },
83
+ { label: 'Deal name', type: 'text', required: false, mapTo: 'deal.name', placeholder: null },
84
+ { label: 'Opportunity name', type: 'text', required: false, mapTo: 'opportunity.name', placeholder: null },
85
+ { label: 'Partnership name', type: 'text', required: false, mapTo: 'partnership.name', placeholder: null },
86
+ ]
87
+
88
+ /** A submission-only entry in the "Add field" menu: shown by control, not by mapping. */
89
+ export interface SubmissionFieldPreset {
90
+ /** What the menu shows. The field itself starts with its own label. */
91
+ readonly menuLabel: string
92
+ readonly field: FormFieldInput
93
+ }
94
+
95
+ /** The submission-only fields "Add field" always offers; they may repeat freely. */
96
+ export const SUBMISSION_FIELD_PRESETS: readonly SubmissionFieldPreset[] = [
97
+ { menuLabel: 'Text', field: NEW_FIELD },
98
+ {
99
+ menuLabel: 'Text area',
100
+ field: { label: 'Message', type: 'textarea', required: false, mapTo: 'submission', placeholder: null },
101
+ },
102
+ {
103
+ menuLabel: 'Select',
104
+ field: {
105
+ label: 'New select',
106
+ type: 'select',
107
+ required: false,
108
+ mapTo: 'submission',
109
+ placeholder: null,
110
+ options: NEW_SELECT_OPTIONS,
111
+ },
112
+ },
113
+ {
114
+ menuLabel: 'Consent',
115
+ field: {
116
+ label: 'Consent',
117
+ type: 'consent',
118
+ required: false,
119
+ mapTo: 'person.consent',
120
+ placeholder: null,
121
+ statement: 'Please tell us how we can contact you.',
122
+ consentPurposeIds: [],
123
+ consentPurposeLabels: {},
124
+ },
125
+ },
126
+ {
127
+ menuLabel: 'Notice',
128
+ field: {
129
+ label: 'Privacy notice',
130
+ type: 'notice',
131
+ required: false,
132
+ mapTo: 'person.consent',
133
+ placeholder: null,
134
+ statement:
135
+ 'By submitting this form you agree to us storing your information and using it to contact you about your enquiry.',
136
+ consentPurposeIds: [],
137
+ consentPurposeLabels: {},
138
+ },
139
+ },
140
+ ]