@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,461 @@
1
+ import { CONSENT_PURPOSE_STATUS_LABELS, CONSENT_PURPOSE_STATUSES } from '@kelpie/schemas'
2
+ import type { ConsentPurpose, ConsentPurposeStatus } from '@kelpie/schemas'
3
+ import { useMemo, useState } from 'react'
4
+ import type { FormEvent } from 'react'
5
+
6
+ import {
7
+ useConsentPurposes,
8
+ useCreateConsentPurpose,
9
+ useDeleteConsentPurpose,
10
+ useUpdateConsentPurpose,
11
+ } from '../../api/resources/consentPurposes.ts'
12
+ import { useSession } from '../../api/resources/session.ts'
13
+ import { AddButton } from '../../components/SectionHeader.tsx'
14
+ import { PageHeader } from '../../components/PageHeader.tsx'
15
+ import { ErrorPanel, LoadingPanel } from '../../components/QueryState.tsx'
16
+
17
+ /**
18
+ * Admin surface for workspace consent purposes.
19
+ *
20
+ * Admin-only, matching Custom fields: a purpose change touches every form,
21
+ * list, and import job that names it, and is team-wide config. A purpose has
22
+ * a default status that every Person without an explicit override inherits.
23
+ */
24
+
25
+ export function PrivacyPage(): React.JSX.Element {
26
+ const { session } = useSession()
27
+ const isAdmin = session?.role === 'owner' || session?.role === 'admin'
28
+
29
+ return (
30
+ <div className="animate-slide-in mx-auto max-w-4xl space-y-6">
31
+ <PageHeader
32
+ title="Privacy"
33
+ description="Consent purposes for the things you contact people about. A form's consent checkbox, a list you add signups to, and an import can each grant one of these — and every Person's record reflects it."
34
+ />
35
+ {isAdmin ? <PurposesAdmin /> : <MemberNotice />}
36
+ </div>
37
+ )
38
+ }
39
+
40
+ function MemberNotice(): React.JSX.Element {
41
+ return (
42
+ <p className="rounded-md border border-border px-4 py-3 text-[13px] text-ink-muted">
43
+ Consent purposes are managed by workspace admins.
44
+ </p>
45
+ )
46
+ }
47
+
48
+ function PurposesAdmin(): React.JSX.Element {
49
+ const [adding, setAdding] = useState(false)
50
+ const [seeding, setSeeding] = useState(false)
51
+ const purposes = useConsentPurposes({ sort: 'sort_order', limit: 200 })
52
+ const create = useCreateConsentPurpose()
53
+
54
+ async function seedStarters(): Promise<void> {
55
+ setSeeding(true)
56
+ try {
57
+ await create.runAsync({
58
+ slug: 'contact',
59
+ label: 'Contact',
60
+ description: 'Being contacted by the workspace about our work together.',
61
+ defaultStatus: 'unknown',
62
+ })
63
+ await create.runAsync({
64
+ slug: 'marketing',
65
+ label: 'Marketing',
66
+ description: 'Marketing communications — newsletters, product updates, and campaigns.',
67
+ defaultStatus: 'unknown',
68
+ })
69
+ } catch {
70
+ // Error rendered inline via the ErrorPanel below.
71
+ } finally {
72
+ setSeeding(false)
73
+ }
74
+ }
75
+
76
+ return (
77
+ <>
78
+ <div className="flex justify-end">
79
+ <AddButton
80
+ onClick={() => {
81
+ setAdding(true)
82
+ }}
83
+ label="Add a purpose"
84
+ />
85
+ </div>
86
+
87
+ {adding && (
88
+ <AddPurposeForm
89
+ onDone={() => {
90
+ setAdding(false)
91
+ }}
92
+ />
93
+ )}
94
+
95
+ {purposes.error !== null && <ErrorPanel error={purposes.error} />}
96
+ {create.error !== null && <ErrorPanel error={create.error} />}
97
+ {purposes.isLoading ? (
98
+ <LoadingPanel label="Loading purposes…" />
99
+ ) : purposes.records.length === 0 ? (
100
+ <EmptyState
101
+ onAdd={() => {
102
+ setAdding(true)
103
+ }}
104
+ onSeedStarters={() => {
105
+ void seedStarters()
106
+ }}
107
+ isSeeding={seeding}
108
+ />
109
+ ) : (
110
+ <ul className="space-y-3">
111
+ {purposes.records.map((purpose) => (
112
+ <li key={purpose.id}>
113
+ <PurposeRow purpose={purpose} />
114
+ </li>
115
+ ))}
116
+ </ul>
117
+ )}
118
+ </>
119
+ )
120
+ }
121
+
122
+ interface EmptyStateProps {
123
+ readonly onAdd: () => void
124
+ readonly onSeedStarters: () => void
125
+ readonly isSeeding: boolean
126
+ }
127
+
128
+ function EmptyState({ onAdd, onSeedStarters, isSeeding }: EmptyStateProps): React.JSX.Element {
129
+ return (
130
+ <div className="rounded-md border border-dashed border-border p-6 text-[13px]">
131
+ <p className="font-medium text-ink">You have no consent purposes yet.</p>
132
+ <p className="mt-1 text-ink-muted">
133
+ A purpose is what a person is agreeing to — being contacted at all, marketing, research.
134
+ Every workspace usually has at least a general <span className="font-medium">Contact</span>{' '}
135
+ purpose. Forms, lists, and imports that don&rsquo;t need consent don&rsquo;t need one.
136
+ </p>
137
+ <div className="mt-4 flex flex-wrap items-center gap-2">
138
+ <button
139
+ type="button"
140
+ onClick={onSeedStarters}
141
+ disabled={isSeeding}
142
+ className="rounded-md bg-accent px-3 py-1.5 text-[12px] font-medium text-white hover:bg-accent-hover disabled:opacity-50"
143
+ >
144
+ {isSeeding ? 'Adding…' : 'Add Contact & Marketing'}
145
+ </button>
146
+ <button
147
+ type="button"
148
+ onClick={onAdd}
149
+ className="rounded-md border border-border px-3 py-1.5 text-[12px] font-medium text-ink hover:border-ink-muted"
150
+ >
151
+ Add my own
152
+ </button>
153
+ </div>
154
+ </div>
155
+ )
156
+ }
157
+
158
+ interface AddPurposeFormProps {
159
+ readonly onDone: () => void
160
+ }
161
+
162
+ function AddPurposeForm({ onDone }: AddPurposeFormProps): React.JSX.Element {
163
+ const create = useCreateConsentPurpose()
164
+ const [label, setLabel] = useState('')
165
+ const [slug, setSlug] = useState('')
166
+ const [slugEdited, setSlugEdited] = useState(false)
167
+ const [description, setDescription] = useState('')
168
+ const [defaultStatus, setDefaultStatus] = useState<ConsentPurposeStatus>('unknown')
169
+
170
+ const derivedSlug = useMemo(
171
+ () => (slugEdited ? slug : slugify(label)),
172
+ [slugEdited, slug, label],
173
+ )
174
+
175
+ function submit(event: FormEvent): void {
176
+ event.preventDefault()
177
+ const trimmedLabel = label.trim()
178
+ if (trimmedLabel.length === 0) {
179
+ return
180
+ }
181
+ create
182
+ .runAsync({
183
+ slug: derivedSlug,
184
+ label: trimmedLabel,
185
+ description: description.trim(),
186
+ defaultStatus,
187
+ })
188
+ .then(() => {
189
+ setLabel('')
190
+ setSlug('')
191
+ setSlugEdited(false)
192
+ setDescription('')
193
+ setDefaultStatus('unknown')
194
+ onDone()
195
+ })
196
+ .catch(() => {
197
+ // Error rendered below.
198
+ })
199
+ }
200
+
201
+ return (
202
+ <form
203
+ onSubmit={submit}
204
+ className="space-y-3 rounded-md border border-border bg-surface-raised p-4"
205
+ >
206
+ <div className="grid gap-3 sm:grid-cols-2">
207
+ <Field label="Label">
208
+ <input
209
+ className="w-full rounded-md border border-border bg-transparent px-2 py-1 text-[13px] outline-none focus:border-accent"
210
+ value={label}
211
+ onChange={(event) => {
212
+ setLabel(event.target.value)
213
+ }}
214
+ required
215
+ />
216
+ </Field>
217
+ <Field label="Slug">
218
+ <input
219
+ className="w-full rounded-md border border-border bg-transparent px-2 py-1 font-mono text-[12px] outline-none focus:border-accent"
220
+ value={derivedSlug}
221
+ onChange={(event) => {
222
+ setSlug(event.target.value)
223
+ setSlugEdited(true)
224
+ }}
225
+ placeholder="lowercase_snake_case"
226
+ pattern="^[a-z][a-z0-9_]*$"
227
+ required
228
+ />
229
+ </Field>
230
+ <Field label="Default status">
231
+ <select
232
+ className="w-full rounded-md border border-border bg-transparent px-2 py-1 text-[13px] outline-none focus:border-accent"
233
+ value={defaultStatus}
234
+ onChange={(event) => {
235
+ setDefaultStatus(event.target.value as ConsentPurposeStatus)
236
+ }}
237
+ >
238
+ {CONSENT_PURPOSE_STATUSES.map((option) => (
239
+ <option key={option} value={option}>
240
+ {CONSENT_PURPOSE_STATUS_LABELS[option]}
241
+ </option>
242
+ ))}
243
+ </select>
244
+ </Field>
245
+ </div>
246
+ <Field label="Description (shown on capture surfaces and to agents)">
247
+ <textarea
248
+ className="min-h-[60px] w-full rounded-md border border-border bg-transparent px-2 py-1 text-[13px] outline-none focus:border-accent"
249
+ value={description}
250
+ onChange={(event) => {
251
+ setDescription(event.target.value)
252
+ }}
253
+ />
254
+ </Field>
255
+
256
+ {create.error !== null && <ErrorPanel error={create.error} />}
257
+
258
+ <div className="flex justify-end gap-2">
259
+ <button
260
+ type="button"
261
+ onClick={() => {
262
+ onDone()
263
+ }}
264
+ className="rounded-md border border-border px-3 py-1 text-[12px] text-ink-muted hover:text-ink"
265
+ >
266
+ Cancel
267
+ </button>
268
+ <button
269
+ type="submit"
270
+ disabled={create.isPending}
271
+ className="rounded-md bg-accent px-3 py-1 text-[12px] font-medium text-white hover:bg-accent-hover disabled:opacity-50"
272
+ >
273
+ {create.isPending ? 'Adding…' : 'Add purpose'}
274
+ </button>
275
+ </div>
276
+ </form>
277
+ )
278
+ }
279
+
280
+ interface PurposeRowProps {
281
+ readonly purpose: ConsentPurpose
282
+ }
283
+
284
+ function PurposeRow({ purpose }: PurposeRowProps): React.JSX.Element {
285
+ const update = useUpdateConsentPurpose()
286
+ const remove = useDeleteConsentPurpose()
287
+ const [editing, setEditing] = useState(false)
288
+ const [label, setLabel] = useState(purpose.label)
289
+ const [description, setDescription] = useState(purpose.description)
290
+ const [defaultStatus, setDefaultStatus] = useState<ConsentPurposeStatus>(purpose.defaultStatus)
291
+ const [confirmingDelete, setConfirmingDelete] = useState(false)
292
+
293
+ function save(event: FormEvent): void {
294
+ event.preventDefault()
295
+ update
296
+ .runAsync({
297
+ id: purpose.id,
298
+ changes: {
299
+ label: label.trim(),
300
+ description: description.trim(),
301
+ defaultStatus,
302
+ },
303
+ })
304
+ .then(() => {
305
+ setEditing(false)
306
+ })
307
+ .catch(() => {
308
+ // Error rendered inline.
309
+ })
310
+ }
311
+
312
+ return (
313
+ <div className="rounded-md border border-border p-3">
314
+ <div className="flex items-start justify-between gap-3">
315
+ <div className="min-w-0 flex-1">
316
+ <div className="flex flex-wrap items-baseline gap-2">
317
+ <div className="text-[14px] font-semibold text-ink">{purpose.label}</div>
318
+ <code className="rounded bg-surface-raised px-1.5 py-0.5 font-mono text-[11px] text-ink-muted">
319
+ {purpose.slug}
320
+ </code>
321
+ <span className="text-[11px] text-ink-muted">
322
+ Default: {CONSENT_PURPOSE_STATUS_LABELS[purpose.defaultStatus]}
323
+ </span>
324
+ </div>
325
+ {purpose.description.length > 0 && (
326
+ <p className="mt-1 text-[12px] text-ink-muted">{purpose.description}</p>
327
+ )}
328
+ </div>
329
+ <div className="flex shrink-0 gap-2">
330
+ <button
331
+ type="button"
332
+ onClick={() => {
333
+ setEditing((current) => !current)
334
+ }}
335
+ className="text-[12px] text-ink-muted hover:text-ink"
336
+ >
337
+ {editing ? 'Cancel' : 'Edit'}
338
+ </button>
339
+ <button
340
+ type="button"
341
+ onClick={() => {
342
+ setConfirmingDelete(true)
343
+ }}
344
+ className="text-[12px] text-red-600 hover:text-red-700"
345
+ >
346
+ Delete
347
+ </button>
348
+ </div>
349
+ </div>
350
+
351
+ {editing && (
352
+ <form onSubmit={save} className="mt-3 space-y-2">
353
+ <Field label="Label">
354
+ <input
355
+ className="w-full rounded-md border border-border bg-transparent px-2 py-1 text-[13px] outline-none focus:border-accent"
356
+ value={label}
357
+ onChange={(event) => {
358
+ setLabel(event.target.value)
359
+ }}
360
+ required
361
+ />
362
+ </Field>
363
+ <Field label="Default status">
364
+ <select
365
+ className="w-full rounded-md border border-border bg-transparent px-2 py-1 text-[13px] outline-none focus:border-accent"
366
+ value={defaultStatus}
367
+ onChange={(event) => {
368
+ setDefaultStatus(event.target.value as ConsentPurposeStatus)
369
+ }}
370
+ >
371
+ {CONSENT_PURPOSE_STATUSES.map((option) => (
372
+ <option key={option} value={option}>
373
+ {CONSENT_PURPOSE_STATUS_LABELS[option]}
374
+ </option>
375
+ ))}
376
+ </select>
377
+ </Field>
378
+ <Field label="Description">
379
+ <textarea
380
+ className="min-h-[60px] w-full rounded-md border border-border bg-transparent px-2 py-1 text-[13px] outline-none focus:border-accent"
381
+ value={description}
382
+ onChange={(event) => {
383
+ setDescription(event.target.value)
384
+ }}
385
+ />
386
+ </Field>
387
+ {update.error !== null && <ErrorPanel error={update.error} />}
388
+ <div className="flex justify-end">
389
+ <button
390
+ type="submit"
391
+ disabled={update.isPending}
392
+ className="rounded-md bg-accent px-3 py-1 text-[12px] font-medium text-white hover:bg-accent-hover disabled:opacity-50"
393
+ >
394
+ {update.isPending ? 'Saving…' : 'Save'}
395
+ </button>
396
+ </div>
397
+ </form>
398
+ )}
399
+
400
+ {confirmingDelete && (
401
+ <div className="mt-3 rounded-md border border-red-300 bg-red-50 p-3 text-[13px] text-red-800 dark:border-red-800 dark:bg-red-950/40 dark:text-red-200">
402
+ <p>
403
+ Delete the {purpose.label} purpose? Remove it from every form field and list that
404
+ names it first — the delete refuses otherwise.
405
+ </p>
406
+ {remove.error !== null && <ErrorPanel error={remove.error} />}
407
+ <div className="mt-2 flex justify-end gap-2">
408
+ <button
409
+ type="button"
410
+ onClick={() => {
411
+ setConfirmingDelete(false)
412
+ }}
413
+ className="rounded-md border border-red-300 px-3 py-1 text-[12px] hover:bg-red-100 dark:border-red-800 dark:hover:bg-red-900/40"
414
+ >
415
+ Cancel
416
+ </button>
417
+ <button
418
+ type="button"
419
+ onClick={() => {
420
+ remove
421
+ .runAsync(purpose.id)
422
+ .then(() => {
423
+ setConfirmingDelete(false)
424
+ })
425
+ .catch(() => {
426
+ // Error rendered inline.
427
+ })
428
+ }}
429
+ disabled={remove.isPending}
430
+ className="rounded-md bg-red-600 px-3 py-1 text-[12px] font-medium text-white hover:bg-red-700 disabled:opacity-50"
431
+ >
432
+ {remove.isPending ? 'Deleting…' : 'Delete'}
433
+ </button>
434
+ </div>
435
+ </div>
436
+ )}
437
+ </div>
438
+ )
439
+ }
440
+
441
+ interface FieldProps {
442
+ readonly label: string
443
+ readonly children: React.ReactNode
444
+ }
445
+
446
+ function Field({ label, children }: FieldProps): React.JSX.Element {
447
+ return (
448
+ <label className="block text-[12px]">
449
+ <span className="mb-1 block font-medium text-ink-muted">{label}</span>
450
+ {children}
451
+ </label>
452
+ )
453
+ }
454
+
455
+ function slugify(label: string): string {
456
+ return label
457
+ .toLowerCase()
458
+ .replace(/[^a-z0-9]+/gu, '_')
459
+ .replace(/^_+|_+$/gu, '')
460
+ .replace(/^([0-9])/u, '_$1')
461
+ }
@@ -7,6 +7,7 @@ import { Chip } from '../../components/Chip.tsx'
7
7
  import type { ChipTone } from '../../components/Chip.tsx'
8
8
  import { DataTable } from '../../components/DataTable.tsx'
9
9
  import type { Column } from '../../components/DataTable.tsx'
10
+ import { Paginator } from '../../components/Paginator.tsx'
10
11
  import { ErrorPanel, LoadingPanel } from '../../components/QueryState.tsx'
11
12
  import { SegmentedControl } from '../../components/SegmentedControl.tsx'
12
13
  import { formatDateTime } from '../../lib/dates.ts'
@@ -138,25 +139,19 @@ export function WebhookDeliveries({ webhookId }: { readonly webhookId: string })
138
139
  {status === 'all' ? 'Nothing delivered yet.' : `No ${status} deliveries.`}
139
140
  </p>
140
141
  ) : (
141
- <DataTable
142
- columns={columns}
143
- rows={deliveries.records}
144
- getRowId={(delivery) => delivery.id}
145
- />
142
+ <>
143
+ <Paginator list={deliveries} placement="top" />
144
+ <DataTable
145
+ columns={columns}
146
+ rows={deliveries.records}
147
+ getRowId={(delivery) => delivery.id}
148
+ />
149
+ </>
146
150
  )}
147
151
 
148
152
  {open !== undefined && <PayloadPanel delivery={open} />}
149
153
 
150
- {deliveries.hasMore && (
151
- <button
152
- type="button"
153
- onClick={deliveries.loadMore}
154
- disabled={deliveries.isLoadingMore}
155
- className="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"
156
- >
157
- {deliveries.isLoadingMore ? 'Loading…' : 'Load more'}
158
- </button>
159
- )}
154
+ <Paginator list={deliveries} />
160
155
  </div>
161
156
  )
162
157
  }
@@ -19,6 +19,7 @@ import {
19
19
  import { Chip } from '../../components/Chip.tsx'
20
20
  import type { ChipTone } from '../../components/Chip.tsx'
21
21
  import { PageHeader } from '../../components/PageHeader.tsx'
22
+ import { Paginator } from '../../components/Paginator.tsx'
22
23
  import { ErrorPanel, LoadingPanel } from '../../components/QueryState.tsx'
23
24
  import { formatDateTime } from '../../lib/dates.ts'
24
25
  import { WebhookDeliveries } from './WebhookDeliveries.tsx'
@@ -68,7 +69,7 @@ function MemberNotice(): React.JSX.Element {
68
69
  }
69
70
 
70
71
  function WebhookAdmin(): React.JSX.Element {
71
- const { records, isLoading, error, hasMore, isLoadingMore, loadMore } = useWebhooks()
72
+ const webhooks = useWebhooks()
72
73
  const [minted, setMinted] = useState<CreatedWebhook | null>(null)
73
74
 
74
75
  return (
@@ -88,30 +89,24 @@ function WebhookAdmin(): React.JSX.Element {
88
89
  />
89
90
  )}
90
91
 
91
- {error !== null && <ErrorPanel error={error} />}
92
+ {webhooks.error !== null && <ErrorPanel error={webhooks.error} />}
92
93
 
93
- {isLoading ? (
94
+ {webhooks.isLoading ? (
94
95
  <LoadingPanel label="Loading webhooks…" />
95
- ) : records.length === 0 ? (
96
+ ) : webhooks.records.length === 0 ? (
96
97
  <p className="text-[13px] text-ink-muted">No webhooks yet.</p>
97
98
  ) : (
98
- <ul className="space-y-3">
99
- {records.map((webhook) => (
100
- <WebhookRow key={webhook.id} webhook={webhook} onRotated={setMinted} />
101
- ))}
102
- </ul>
99
+ <>
100
+ <Paginator list={webhooks} placement="top" />
101
+ <ul className="space-y-3">
102
+ {webhooks.records.map((webhook) => (
103
+ <WebhookRow key={webhook.id} webhook={webhook} onRotated={setMinted} />
104
+ ))}
105
+ </ul>
106
+ </>
103
107
  )}
104
108
 
105
- {hasMore && (
106
- <button
107
- type="button"
108
- disabled={isLoadingMore}
109
- onClick={loadMore}
110
- className="rounded-md border border-border px-3 py-1.5 text-[12px] font-medium text-ink-muted transition hover:text-ink disabled:opacity-50"
111
- >
112
- {isLoadingMore ? 'Loading…' : 'Load more'}
113
- </button>
114
- )}
109
+ <Paginator list={webhooks} />
115
110
  </>
116
111
  )
117
112
  }