@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,554 @@
1
+ import type { Enquiry, EnquiryInput, PipelineStage } from '@kelpie/schemas'
2
+ import { useState } from 'react'
3
+ import type { FormEvent } from 'react'
4
+ import { Link, useNavigate, useParams } from 'react-router'
5
+
6
+ import { usePatch } from '../api/resource.ts'
7
+ import type { PatchResult } from '../api/resource.ts'
8
+ import { useCompanies, useCompany } from '../api/resources/companies.ts'
9
+ import {
10
+ useConvertEnquiry,
11
+ useDeleteEnquiry,
12
+ useEnquiry,
13
+ useUpdateEnquiry,
14
+ } from '../api/resources/enquiries.ts'
15
+ import { useFormSubmissionsForRecord } from '../api/resources/forms.ts'
16
+ import { useMembers } from '../api/resources/members.ts'
17
+ import { usePeople } from '../api/resources/people.ts'
18
+ import { usePipelineStages } from '../api/resources/pipelineStages.ts'
19
+ import { useRecordPlanItems } from '../api/resources/planItems.ts'
20
+ import { ActivitiesPanel, LatestActivity } from '../components/ActivitiesPanel.tsx'
21
+ import { AgentTasks } from '../components/AgentTasks.tsx'
22
+ import { Chip } from '../components/Chip.tsx'
23
+ import type { ChipTone } from '../components/Chip.tsx'
24
+ import { DecisionsPanel } from '../components/DecisionsPanel.tsx'
25
+ import { DeleteRecord } from '../components/DeleteRecord.tsx'
26
+ import { EntitySearch } from '../components/EntitySearch.tsx'
27
+ import { FormsPanel } from '../components/FormsPanel.tsx'
28
+ import { CustomFieldsPanel } from '../components/CustomFieldsPanel.tsx'
29
+ import { useHasCustomFields } from '../components/useHasCustomFields.ts'
30
+ import { InlineEdit } from '../components/InlineEdit.tsx'
31
+ import { ListsPanel } from '../components/ListsPanel.tsx'
32
+ import { NotesPanel } from '../components/NotesPanel.tsx'
33
+ import { PlanAttention } from '../components/PlanAttention.tsx'
34
+ import { PlanPanel } from '../components/PlanPanel.tsx'
35
+ import { ErrorPanel, LoadingPanel, NotFoundPanel } from '../components/QueryState.tsx'
36
+ import { RecordTabs } from '../components/RecordTabs.tsx'
37
+ import type { RecordTabDescriptor } from '../components/RecordTabs.tsx'
38
+ import { SectionHeader } from '../components/SectionHeader.tsx'
39
+ import { SidebarField } from '../components/SidebarField.tsx'
40
+ import { SummaryBlock } from '../components/SummaryBlock.tsx'
41
+ import { useRecordTabs } from '../registry/context.ts'
42
+ import { inSlotOrder } from '../registry/registry.ts'
43
+ import { toTags } from './fields.ts'
44
+
45
+ /**
46
+ * One enquiry.
47
+ *
48
+ * Same shape as `OpportunityDetail` minus the target date, plus a
49
+ * Convert-to-Deal action that becomes a link to the created deal once the
50
+ * enquiry has been converted.
51
+ */
52
+
53
+ const STAGE_TONES: Readonly<Record<string, ChipTone>> = {
54
+ new: 'accent',
55
+ in_progress: 'warning',
56
+ closed: 'neutral',
57
+ }
58
+
59
+ export function EnquiryDetail(): React.JSX.Element {
60
+ const { id } = useParams()
61
+ const navigate = useNavigate()
62
+ const { record, isLoading, isNotFound, error } = useEnquiry(id)
63
+ const deleteEnquiry = useDeleteEnquiry()
64
+ const convertEnquiry = useConvertEnquiry()
65
+ const moduleTabs = inSlotOrder(useRecordTabs('enquiry'))
66
+ const hasCustomFields = useHasCustomFields('enquiry')
67
+ const [activeTab, setActiveTab] = useState('overview')
68
+ const [showConvert, setShowConvert] = useState(false)
69
+ const formSubmissions = useFormSubmissionsForRecord('enquiry', id)
70
+
71
+ if (isNotFound) {
72
+ return <NotFoundPanel label="Enquiry" backTo="/enquiries" />
73
+ }
74
+
75
+ if (error !== null) {
76
+ return <ErrorPanel error={error} />
77
+ }
78
+
79
+ if (isLoading || record === undefined || id === undefined) {
80
+ return <LoadingPanel label="Loading enquiry…" />
81
+ }
82
+
83
+ const tabs: readonly RecordTabDescriptor<string>[] = [
84
+ { id: 'overview', label: 'Overview' },
85
+ ...(hasCustomFields ? [{ id: 'fields', label: 'Fields' }] : []),
86
+ { id: 'plan', label: 'Plan' },
87
+ { id: 'activity', label: 'Activity' },
88
+ { id: 'notes', label: 'Notes' },
89
+ { id: 'decisions', label: 'Decisions' },
90
+ { id: 'lists', label: 'Lists' },
91
+ ...(formSubmissions.records.length === 0
92
+ ? []
93
+ : [{ id: 'forms', label: 'Forms', count: formSubmissions.records.length }]),
94
+ ...moduleTabs.map((tab) => ({ id: tab.id, label: tab.label })),
95
+ ]
96
+ const active = tabs.some((tab) => tab.id === activeTab) ? activeTab : 'overview'
97
+ const moduleTab = moduleTabs.find((tab) => tab.id === active)
98
+
99
+ const convertBlockedReason =
100
+ record.convertedDealId !== null
101
+ ? null
102
+ : record.companyId === null
103
+ ? 'Link a company first'
104
+ : null
105
+
106
+ return (
107
+ <div className="animate-fade-in mx-auto max-w-6xl">
108
+ <Link
109
+ to="/enquiries"
110
+ className="mb-4 inline-flex text-[12px] font-medium text-ink-muted transition hover:text-accent"
111
+ >
112
+ ← Enquiries
113
+ </Link>
114
+
115
+ <div className="grid items-start gap-8 lg:grid-cols-[minmax(0,1fr)_280px]">
116
+ <div className="min-w-0 space-y-8">
117
+ <EnquiryHeading enquiry={record} />
118
+
119
+ <div className="flex justify-end gap-2">
120
+ {record.convertedDealId === null ? (
121
+ <button
122
+ type="button"
123
+ onClick={() => {
124
+ if (convertBlockedReason === null) {
125
+ setShowConvert(true)
126
+ }
127
+ }}
128
+ disabled={convertBlockedReason !== null || convertEnquiry.isPending}
129
+ title={convertBlockedReason ?? undefined}
130
+ className="rounded-md border border-border px-2.5 py-1 text-[12px] font-medium text-ink-muted transition hover:border-accent hover:text-accent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:border-border disabled:hover:text-ink-muted"
131
+ >
132
+ Convert to deal
133
+ </button>
134
+ ) : (
135
+ <Link
136
+ to={`/deals/${record.convertedDealId}`}
137
+ className="rounded-md border border-border px-2.5 py-1 text-[12px] font-medium text-accent transition hover:border-accent hover:underline"
138
+ >
139
+ Converted — view deal
140
+ </Link>
141
+ )}
142
+ <AgentTasks targetType="enquiry" targetId={record.id} targetLabel={record.name} />
143
+ <DeleteRecord
144
+ recordLabel="Enquiry"
145
+ recordName={record.name}
146
+ isPending={deleteEnquiry.isPending}
147
+ error={deleteEnquiry.error}
148
+ onConfirm={() => {
149
+ deleteEnquiry
150
+ .runAsync(record.id)
151
+ .then(() => navigate('/enquiries'))
152
+ .catch(() => undefined)
153
+ }}
154
+ />
155
+ </div>
156
+
157
+ {convertEnquiry.error !== null && <ErrorPanel error={convertEnquiry.error} />}
158
+ {showConvert && (
159
+ <ConvertConfirm
160
+ enquiry={record}
161
+ isPending={convertEnquiry.isPending}
162
+ onCancel={() => {
163
+ setShowConvert(false)
164
+ }}
165
+ onConfirm={() => {
166
+ convertEnquiry
167
+ .runAsync(record.id)
168
+ .then((deal) => {
169
+ setShowConvert(false)
170
+
171
+ return navigate(`/deals/${deal.id}`)
172
+ })
173
+ .catch(() => undefined)
174
+ }}
175
+ />
176
+ )}
177
+
178
+ <RecordTabs
179
+ tabs={tabs}
180
+ active={active}
181
+ onChange={setActiveTab}
182
+ ariaLabel="Enquiry sections"
183
+ >
184
+ {active === 'overview' && <EnquiryOverview enquiry={record} />}
185
+ {active === 'fields' && <EnquiryFields enquiry={record} />}
186
+ {active === 'plan' && <PlanPanel targetType="enquiry" targetId={record.id} />}
187
+ {active === 'activity' && (
188
+ <ActivitiesPanel targetType="enquiry" targetId={record.id} />
189
+ )}
190
+ {active === 'notes' && <NotesPanel targetType="enquiry" targetId={record.id} />}
191
+ {active === 'decisions' && (
192
+ <DecisionsPanel targetType="enquiry" targetId={record.id} />
193
+ )}
194
+ {active === 'lists' && <ListsPanel targetType="enquiry" targetId={record.id} />}
195
+ {active === 'forms' && <FormsPanel targetType="enquiry" targetId={record.id} />}
196
+ {moduleTab?.render({ objectType: 'enquiry', recordId: record.id })}
197
+ </RecordTabs>
198
+ </div>
199
+
200
+ <aside className="space-y-4 text-[12px] lg:sticky lg:top-6">
201
+ <EnquirySidebar enquiry={record} />
202
+ <EnquiryContacts enquiry={record} />
203
+ </aside>
204
+ </div>
205
+ </div>
206
+ )
207
+ }
208
+
209
+ function useEnquiryPatch(enquiry: Enquiry): PatchResult<EnquiryInput> {
210
+ return usePatch(useUpdateEnquiry, enquiry)
211
+ }
212
+
213
+ function EnquiryHeading({ enquiry }: { readonly enquiry: Enquiry }): React.JSX.Element {
214
+ const { patch, error } = useEnquiryPatch(enquiry)
215
+
216
+ return (
217
+ <div className="min-w-0 flex-1">
218
+ {error !== null && (
219
+ <div className="mb-2">
220
+ <ErrorPanel error={error} />
221
+ </div>
222
+ )}
223
+ <InlineEdit
224
+ value={enquiry.name}
225
+ onChange={(name) => {
226
+ patch({ name })
227
+ }}
228
+ displayClassName="text-[22px] font-semibold tracking-tight text-ink not-italic"
229
+ emptyLabel="Untitled enquiry"
230
+ />
231
+ <div className="mt-1">
232
+ <InlineEdit
233
+ value={enquiry.source}
234
+ onChange={(source) => {
235
+ patch({ source })
236
+ }}
237
+ displayClassName="text-[13px] text-ink-muted not-italic"
238
+ emptyLabel="Add source…"
239
+ />
240
+ </div>
241
+ </div>
242
+ )
243
+ }
244
+
245
+ function EnquiryOverview({ enquiry }: { readonly enquiry: Enquiry }): React.JSX.Element {
246
+ const { patch, error } = useEnquiryPatch(enquiry)
247
+ const planItems = useRecordPlanItems('enquiry', enquiry.id)
248
+
249
+ return (
250
+ <div className="space-y-8">
251
+ {error !== null && <ErrorPanel error={error} />}
252
+ <SummaryBlock
253
+ value={enquiry.summary}
254
+ onChange={(summary) => {
255
+ patch({ summary })
256
+ }}
257
+ />
258
+
259
+ <PlanAttention items={planItems.records} isLoading={planItems.isLoading} />
260
+
261
+ <LatestActivity targetType="enquiry" targetId={enquiry.id} />
262
+ </div>
263
+ )
264
+ }
265
+
266
+ function EnquirySidebar({ enquiry }: { readonly enquiry: Enquiry }): React.JSX.Element {
267
+ const { patch, error } = useEnquiryPatch(enquiry)
268
+ const stages = usePipelineStages('enquiry')
269
+ const members = useMembers()
270
+ const currentCompany = useCompany(enquiry.companyId ?? undefined)
271
+
272
+ const [companySearch, setCompanySearch] = useState('')
273
+ const searchableCompanies = useCompanies({
274
+ term: companySearch.trim().length > 0 ? companySearch.trim() : undefined,
275
+ })
276
+
277
+ const orderedStages = [...stages.records].sort(
278
+ (a: PipelineStage, b: PipelineStage) => a.sortOrder - b.sortOrder,
279
+ )
280
+ const currentStage = orderedStages.find((stage) => stage.id === enquiry.stageId)
281
+
282
+ const companyOptions = [
283
+ ...(currentCompany.record === undefined
284
+ ? []
285
+ : [{ id: currentCompany.record.id, label: currentCompany.record.name }]),
286
+ ...searchableCompanies.records
287
+ .filter((company) => company.id !== enquiry.companyId)
288
+ .map((company) => ({
289
+ id: company.id,
290
+ label: company.name,
291
+ meta: company.domain ?? undefined,
292
+ })),
293
+ ]
294
+
295
+ return (
296
+ <section className="rounded-md border border-border p-3">
297
+ {error !== null && (
298
+ <div className="mb-2">
299
+ <ErrorPanel error={error} />
300
+ </div>
301
+ )}
302
+ <div className="mb-2">
303
+ <div className="mb-0.5 text-[10px] font-semibold tracking-wide text-ink-faint uppercase">
304
+ Stage
305
+ </div>
306
+ <InlineEdit
307
+ value={enquiry.stageId}
308
+ onChange={(stageId) => {
309
+ patch({ stageId })
310
+ }}
311
+ options={orderedStages.map((stage) => ({ value: stage.id, label: stage.label }))}
312
+ display={
313
+ <Chip tone={STAGE_TONES[currentStage?.slug ?? ''] ?? 'accent'}>
314
+ <span className="text-[10px]">{currentStage?.label ?? enquiry.stageId}</span>
315
+ </Chip>
316
+ }
317
+ displayClassName="not-italic inline-flex"
318
+ className="!w-auto"
319
+ />
320
+ </div>
321
+
322
+ <SidebarField label="Organisation">
323
+ <EntitySearch
324
+ options={companyOptions}
325
+ value={enquiry.companyId ?? ''}
326
+ onChange={(companyId) => {
327
+ patch({ companyId: companyId.length > 0 ? companyId : null })
328
+ }}
329
+ onQueryChange={setCompanySearch}
330
+ placeholder="Search companies…"
331
+ size="sm"
332
+ />
333
+ {currentCompany.record !== undefined && (
334
+ <Link
335
+ to={`/companies/${currentCompany.record.id}`}
336
+ className="mt-1 inline-block text-[11px] text-accent hover:underline"
337
+ >
338
+ Open company
339
+ </Link>
340
+ )}
341
+ </SidebarField>
342
+ <SidebarField label="Owner">
343
+ <EntitySearch
344
+ options={members.members.map((member) => ({
345
+ id: member.id,
346
+ label: member.name,
347
+ meta: member.email,
348
+ }))}
349
+ value={enquiry.ownerId ?? ''}
350
+ onChange={(ownerId) => {
351
+ patch({ ownerId })
352
+ }}
353
+ placeholder="Search owners…"
354
+ size="sm"
355
+ />
356
+ </SidebarField>
357
+ <SidebarField label="Tags">
358
+ <InlineEdit
359
+ value={enquiry.tags.join(', ')}
360
+ onChange={(value) => {
361
+ patch({ tags: toTags(value) })
362
+ }}
363
+ display={
364
+ enquiry.tags.length > 0 ? (
365
+ <span className="flex flex-wrap gap-1">
366
+ {enquiry.tags.map((tag) => (
367
+ <Chip key={tag}>
368
+ <span className="text-[10px]">{tag}</span>
369
+ </Chip>
370
+ ))}
371
+ </span>
372
+ ) : undefined
373
+ }
374
+ emptyLabel="Add tags…"
375
+ displayClassName="not-italic"
376
+ />
377
+ </SidebarField>
378
+ </section>
379
+ )
380
+ }
381
+
382
+ function EnquiryFields({ enquiry }: { readonly enquiry: Enquiry }): React.JSX.Element {
383
+ const { patch, error } = useEnquiryPatch(enquiry)
384
+ return (
385
+ <div className="space-y-4">
386
+ {error !== null && <ErrorPanel error={error} />}
387
+ <CustomFieldsPanel
388
+ objectType="enquiry"
389
+ values={enquiry.customFields}
390
+ onPatch={(customFields) => {
391
+ patch({ customFields })
392
+ }}
393
+ />
394
+ </div>
395
+ )
396
+ }
397
+
398
+ function EnquiryContacts({ enquiry }: { readonly enquiry: Enquiry }): React.JSX.Element {
399
+ const { patch, error } = useEnquiryPatch(enquiry)
400
+
401
+ const [adding, setAdding] = useState(false)
402
+ const [personId, setPersonId] = useState('')
403
+ const [search, setSearch] = useState('')
404
+
405
+ const directory = usePeople({ limit: 200 })
406
+ const searchable = usePeople({ term: search.trim().length > 0 ? search.trim() : undefined })
407
+ const nameById = new Map(directory.records.map((person) => [person.id, person.name]))
408
+ const linked = new Set(enquiry.personIds)
409
+
410
+ function reset(): void {
411
+ setAdding(false)
412
+ setPersonId('')
413
+ setSearch('')
414
+ }
415
+
416
+ function submit(event: FormEvent): void {
417
+ event.preventDefault()
418
+
419
+ if (personId.length === 0 || linked.has(personId)) {
420
+ return
421
+ }
422
+
423
+ patch({ personIds: [...enquiry.personIds, personId] })
424
+ reset()
425
+ }
426
+
427
+ return (
428
+ <section className="rounded-md border border-border">
429
+ <div className="border-b border-border px-3.5 py-2.5">
430
+ <SectionHeader
431
+ title="Contacts"
432
+ onAdd={() => {
433
+ setAdding((current) => !current)
434
+ }}
435
+ addLabel="Add contact"
436
+ compact
437
+ />
438
+ </div>
439
+
440
+ {error !== null && (
441
+ <div className="px-3.5 py-2">
442
+ <ErrorPanel error={error} />
443
+ </div>
444
+ )}
445
+
446
+ <ul className="divide-y divide-border">
447
+ {enquiry.personIds.length === 0 && !adding && (
448
+ <li className="px-3.5 py-4 text-[12px] text-ink-faint">No contacts yet.</li>
449
+ )}
450
+ {enquiry.personIds.map((linkedPersonId) => (
451
+ <li
452
+ key={linkedPersonId}
453
+ className="flex items-start justify-between gap-2 px-3.5 py-2.5"
454
+ >
455
+ <Link
456
+ to={`/people/${linkedPersonId}`}
457
+ className="text-[13px] font-medium text-ink hover:text-accent"
458
+ >
459
+ {nameById.get(linkedPersonId) ?? linkedPersonId}
460
+ </Link>
461
+ <button
462
+ type="button"
463
+ onClick={() => {
464
+ patch({
465
+ personIds: enquiry.personIds.filter(
466
+ (candidate) => candidate !== linkedPersonId,
467
+ ),
468
+ })
469
+ }}
470
+ className="text-[11px] font-medium text-danger hover:underline"
471
+ >
472
+ Remove
473
+ </button>
474
+ </li>
475
+ ))}
476
+ </ul>
477
+
478
+ {adding && (
479
+ <form onSubmit={submit} className="space-y-2 border-t border-border bg-surface px-3.5 py-3">
480
+ <EntitySearch
481
+ options={searchable.records
482
+ .filter((person) => !linked.has(person.id))
483
+ .map((person) => ({
484
+ id: person.id,
485
+ label: person.name,
486
+ meta: person.email ?? undefined,
487
+ }))}
488
+ value={personId}
489
+ onChange={setPersonId}
490
+ onQueryChange={setSearch}
491
+ placeholder="Search people…"
492
+ emptyMessage="No people match"
493
+ required
494
+ />
495
+ <div className="flex justify-end gap-2">
496
+ <button
497
+ type="button"
498
+ onClick={reset}
499
+ className="rounded-md px-2.5 py-1 text-[12px] font-medium text-ink-muted hover:text-ink"
500
+ >
501
+ Cancel
502
+ </button>
503
+ <button
504
+ type="submit"
505
+ className="rounded-md bg-accent px-3 py-1.5 text-[12px] font-semibold text-accent-fg hover:bg-accent-hover"
506
+ >
507
+ Add
508
+ </button>
509
+ </div>
510
+ </form>
511
+ )}
512
+ </section>
513
+ )
514
+ }
515
+
516
+ function ConvertConfirm({
517
+ enquiry,
518
+ isPending,
519
+ onCancel,
520
+ onConfirm,
521
+ }: {
522
+ readonly enquiry: Enquiry
523
+ readonly isPending: boolean
524
+ readonly onCancel: () => void
525
+ readonly onConfirm: () => void
526
+ }): React.JSX.Element {
527
+ return (
528
+ <div className="rounded-md border border-border bg-surface-raised p-4">
529
+ <p className="text-[13px] text-ink">
530
+ Convert <span className="font-semibold">{enquiry.name}</span> to a Deal. This creates a new
531
+ deal (name, company, owner and linked people carry over) and moves this enquiry to the
532
+ first closed stage.
533
+ </p>
534
+ <div className="mt-3 flex justify-end gap-2">
535
+ <button
536
+ type="button"
537
+ onClick={onCancel}
538
+ disabled={isPending}
539
+ className="rounded-md px-2.5 py-1 text-[12px] font-medium text-ink-muted hover:text-ink disabled:opacity-50"
540
+ >
541
+ Cancel
542
+ </button>
543
+ <button
544
+ type="button"
545
+ onClick={onConfirm}
546
+ disabled={isPending}
547
+ className="rounded-md bg-accent px-3 py-1.5 text-[12px] font-semibold text-accent-fg hover:bg-accent-hover disabled:opacity-50"
548
+ >
549
+ {isPending ? 'Converting…' : 'Convert'}
550
+ </button>
551
+ </div>
552
+ </div>
553
+ )
554
+ }
@@ -0,0 +1,63 @@
1
+ import {
2
+ useCreatePipelineStage,
3
+ usePipelineStages,
4
+ useRemovePipelineStage,
5
+ useUpdatePipelineStage,
6
+ } from '../api/resources/pipelineStages.ts'
7
+ import { ErrorPanel, LoadingPanel } from '../components/QueryState.tsx'
8
+ import { StageSettings } from '../components/StageSettings.tsx'
9
+
10
+ /** The enquiry pipeline's stage settings, on the shared `StageSettings` editor. */
11
+ export function EnquiryStageSettingsPage(): React.JSX.Element {
12
+ const stages = usePipelineStages('enquiry')
13
+ const createStage = useCreatePipelineStage()
14
+ const updateStage = useUpdatePipelineStage()
15
+ const removeStage = useRemovePipelineStage()
16
+
17
+ if (stages.error !== null) {
18
+ return <ErrorPanel error={stages.error} />
19
+ }
20
+
21
+ if (stages.isLoading) {
22
+ return <LoadingPanel label="Loading stages…" />
23
+ }
24
+
25
+ const ordered = [...stages.records].sort((a, b) => a.sortOrder - b.sortOrder)
26
+ const writeError = createStage.error ?? updateStage.error ?? removeStage.error
27
+
28
+ return (
29
+ <>
30
+ {writeError !== null && (
31
+ <div className="mx-auto mb-3 max-w-xl">
32
+ <ErrorPanel error={writeError} />
33
+ </div>
34
+ )}
35
+ <StageSettings
36
+ title="Enquiry stages"
37
+ backTo="/enquiries"
38
+ backLabel="Enquiries"
39
+ recordNoun="enquiry"
40
+ stages={ordered}
41
+ onRename={(id, label) => {
42
+ updateStage.run({ id, changes: { label } })
43
+ }}
44
+ onReorder={(activeId, overId) => {
45
+ const position = ordered.findIndex((stage) => stage.id === overId)
46
+
47
+ if (position >= 0) {
48
+ updateStage.run({ id: activeId, changes: { sortOrder: position } })
49
+ }
50
+ }}
51
+ onToggleOpen={(id, open) => {
52
+ updateStage.run({ id, changes: { open } })
53
+ }}
54
+ onAdd={(label) => {
55
+ createStage.run({ kind: 'enquiry', label })
56
+ }}
57
+ onRemove={(id, moveToId) => {
58
+ removeStage.run({ id, moveToId })
59
+ }}
60
+ />
61
+ </>
62
+ )
63
+ }
@@ -10,6 +10,7 @@ import { ErrorPanel, LoadingPanel, NotFoundPanel } from '../components/QueryStat
10
10
  import { RecordTabs } from '../components/RecordTabs.tsx'
11
11
  import { EmbedPanel } from './forms/EmbedPanel.tsx'
12
12
  import { FieldsEditor } from './forms/FieldsEditor.tsx'
13
+ import { FormActions } from './forms/FormActions.tsx'
13
14
  import { FormSettings } from './forms/FormSettings.tsx'
14
15
  import { SubmissionsTable } from './forms/SubmissionsTable.tsx'
15
16
 
@@ -21,7 +22,7 @@ import { SubmissionsTable } from './forms/SubmissionsTable.tsx'
21
22
  * committing per keystroke would rewrite every field on every character.
22
23
  */
23
24
 
24
- type FormTab = 'submissions' | 'fields' | 'settings' | 'embed'
25
+ type FormTab = 'submissions' | 'fields' | 'actions' | 'settings' | 'embed'
25
26
 
26
27
  export function FormDetail(): React.JSX.Element {
27
28
  const { id } = useParams()
@@ -72,6 +73,7 @@ export function FormDetail(): React.JSX.Element {
72
73
  tabs={[
73
74
  { id: 'submissions', label: 'Submissions', count: submissions.records.length },
74
75
  { id: 'fields', label: 'Fields', count: record.fields.length },
76
+ { id: 'actions', label: 'Actions', count: actionCount(record) },
75
77
  { id: 'settings', label: 'Settings' },
76
78
  { id: 'embed', label: 'Embed' },
77
79
  ]}
@@ -81,6 +83,7 @@ export function FormDetail(): React.JSX.Element {
81
83
  >
82
84
  {tab === 'submissions' && <SubmissionsTable form={record} submissions={submissions} />}
83
85
  {tab === 'fields' && <FieldsEditor form={record} />}
86
+ {tab === 'actions' && <FormActions form={record} />}
84
87
  {tab === 'settings' && <FormSettings form={record} />}
85
88
  {tab === 'embed' && <EmbedPanel form={record} />}
86
89
  </RecordTabs>
@@ -88,6 +91,25 @@ export function FormDetail(): React.JSX.Element {
88
91
  )
89
92
  }
90
93
 
94
+ /**
95
+ * How many post-submit actions the form is configured to run. Toggled
96
+ * triggers, tag lists (each counted once when non-empty), lists, and attach
97
+ * targets all contribute. The tab count is a hint, not a total — a reader
98
+ * sees "3" and knows the form does more than just capture answers.
99
+ */
100
+ function actionCount(form: Form): number {
101
+ return (
102
+ (form.createDeal ? 1 : 0) +
103
+ (form.createOpportunity ? 1 : 0) +
104
+ (form.createPartnership ? 1 : 0) +
105
+ (form.createEnquiry ? 1 : 0) +
106
+ (form.personTags.length > 0 ? 1 : 0) +
107
+ (form.companyTags.length > 0 ? 1 : 0) +
108
+ form.listIds.length +
109
+ form.attachTargets.length
110
+ )
111
+ }
112
+
91
113
  /** The title block: name, description, and the two things a reader checks first. */
92
114
  function FormHeading({ form }: { readonly form: Form }): React.JSX.Element {
93
115
  const updateForm = useUpdateForm()