@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,455 @@
1
+ import {
2
+ CUSTOM_FIELD_OBJECT_TYPE_LABELS,
3
+ CUSTOM_FIELD_OBJECT_TYPES,
4
+ CUSTOM_FIELD_TYPE_LABELS,
5
+ CUSTOM_FIELD_TYPES,
6
+ CUSTOM_FIELD_TYPES_WITH_OPTIONS,
7
+ } from '@kelpie/schemas'
8
+ import type {
9
+ CustomFieldDefinition,
10
+ CustomFieldObjectType,
11
+ CustomFieldType,
12
+ } from '@kelpie/schemas'
13
+ import { useMemo, useState } from 'react'
14
+ import type { FormEvent } from 'react'
15
+
16
+ import {
17
+ useCreateCustomField,
18
+ useCustomFields,
19
+ useDeleteCustomField,
20
+ useUpdateCustomField,
21
+ } from '../../api/resources/customFields.ts'
22
+ import { useSession } from '../../api/resources/session.ts'
23
+ import { AddButton } from '../../components/SectionHeader.tsx'
24
+ import { PageHeader } from '../../components/PageHeader.tsx'
25
+ import { ErrorPanel, LoadingPanel } from '../../components/QueryState.tsx'
26
+ import { SegmentedControl } from '../../components/SegmentedControl.tsx'
27
+
28
+ /**
29
+ * Admin surface for workspace-defined fields.
30
+ *
31
+ * Admin-only, matching the pattern from `WebhooksPage` — a definition changes
32
+ * the shape of every record of one object type and is team-wide config, not a
33
+ * per-user preference.
34
+ *
35
+ * Reorder in v1 is server-side via `PATCH sort_order`; a drag-and-drop UI is
36
+ * the next tweak and does not change the wire.
37
+ */
38
+
39
+ const OBJECT_TYPE_OPTIONS: readonly { id: CustomFieldObjectType; label: string }[] =
40
+ CUSTOM_FIELD_OBJECT_TYPES.map((id) => ({
41
+ id,
42
+ label: CUSTOM_FIELD_OBJECT_TYPE_LABELS[id],
43
+ }))
44
+
45
+ const OPTIONS_TYPES: ReadonlySet<CustomFieldType> = new Set(CUSTOM_FIELD_TYPES_WITH_OPTIONS)
46
+
47
+ export function FieldsPage(): React.JSX.Element {
48
+ const { session } = useSession()
49
+ const isAdmin = session?.role === 'owner' || session?.role === 'admin'
50
+
51
+ return (
52
+ <div className="animate-slide-in mx-auto max-w-4xl space-y-6">
53
+ <PageHeader
54
+ title="Custom fields"
55
+ description="Add workspace-defined fields to the six main record types. Values on records go through the same API the UI uses."
56
+ />
57
+ {isAdmin ? <FieldsAdmin /> : <MemberNotice />}
58
+ </div>
59
+ )
60
+ }
61
+
62
+ function MemberNotice(): React.JSX.Element {
63
+ return (
64
+ <p className="rounded-md border border-border px-4 py-3 text-[13px] text-ink-muted">
65
+ Custom fields are managed by workspace admins.
66
+ </p>
67
+ )
68
+ }
69
+
70
+ function FieldsAdmin(): React.JSX.Element {
71
+ const [objectType, setObjectType] = useState<CustomFieldObjectType>('deal')
72
+ const [adding, setAdding] = useState(false)
73
+ const definitions = useCustomFields({ objectType, sort: 'sort_order', limit: 200 })
74
+
75
+ return (
76
+ <>
77
+ <div className="flex flex-wrap items-center gap-3">
78
+ <SegmentedControl
79
+ value={objectType}
80
+ onChange={setObjectType}
81
+ options={OBJECT_TYPE_OPTIONS}
82
+ ariaLabel="Record type"
83
+ />
84
+ <div className="ml-auto">
85
+ <AddButton
86
+ onClick={() => {
87
+ setAdding(true)
88
+ }}
89
+ label="New field"
90
+ />
91
+ </div>
92
+ </div>
93
+
94
+ {adding && (
95
+ <AddFieldForm
96
+ objectType={objectType}
97
+ onDone={() => {
98
+ setAdding(false)
99
+ }}
100
+ />
101
+ )}
102
+
103
+ {definitions.error !== null && <ErrorPanel error={definitions.error} />}
104
+ {definitions.isLoading ? (
105
+ <LoadingPanel label="Loading fields…" />
106
+ ) : definitions.records.length === 0 ? (
107
+ <p className="text-[13px] text-ink-muted">
108
+ No custom fields on {CUSTOM_FIELD_OBJECT_TYPE_LABELS[objectType].toLowerCase()} records yet.
109
+ </p>
110
+ ) : (
111
+ <ul className="space-y-3">
112
+ {definitions.records.map((definition) => (
113
+ <li key={definition.id}>
114
+ <FieldRow definition={definition} />
115
+ </li>
116
+ ))}
117
+ </ul>
118
+ )}
119
+ </>
120
+ )
121
+ }
122
+
123
+ interface AddFieldFormProps {
124
+ readonly objectType: CustomFieldObjectType
125
+ readonly onDone: () => void
126
+ }
127
+
128
+ function AddFieldForm({ objectType, onDone }: AddFieldFormProps): React.JSX.Element {
129
+ const create = useCreateCustomField()
130
+ const [label, setLabel] = useState('')
131
+ const [key, setKey] = useState('')
132
+ const [keyEdited, setKeyEdited] = useState(false)
133
+ const [type, setType] = useState<CustomFieldType>('text')
134
+ const [optionsText, setOptionsText] = useState('')
135
+ const [description, setDescription] = useState('')
136
+
137
+ const derivedKey = useMemo(() => (keyEdited ? key : slugify(label)), [keyEdited, key, label])
138
+
139
+ function submit(event: FormEvent): void {
140
+ event.preventDefault()
141
+ const trimmedLabel = label.trim()
142
+ if (trimmedLabel.length === 0) {
143
+ return
144
+ }
145
+ const options = OPTIONS_TYPES.has(type)
146
+ ? optionsText
147
+ .split(/[\n,]/u)
148
+ .map((option) => option.trim())
149
+ .filter((option) => option.length > 0)
150
+ : []
151
+ create.runAsync({
152
+ objectType,
153
+ key: derivedKey,
154
+ label: trimmedLabel,
155
+ type,
156
+ options,
157
+ description: description.trim(),
158
+ })
159
+ .then(() => {
160
+ setLabel('')
161
+ setKey('')
162
+ setKeyEdited(false)
163
+ setType('text')
164
+ setOptionsText('')
165
+ setDescription('')
166
+ onDone()
167
+ })
168
+ .catch(() => {
169
+ // The mutation error is rendered below the form; keep the values.
170
+ })
171
+ }
172
+
173
+ return (
174
+ <form
175
+ onSubmit={submit}
176
+ className="space-y-3 rounded-md border border-border bg-surface-raised p-4"
177
+ >
178
+ <div className="grid gap-3 sm:grid-cols-2">
179
+ <Field label="Label">
180
+ <input
181
+ className="w-full rounded-md border border-border bg-transparent px-2 py-1 text-[13px] outline-none focus:border-accent"
182
+ value={label}
183
+ onChange={(event) => {
184
+ setLabel(event.target.value)
185
+ }}
186
+ required
187
+ />
188
+ </Field>
189
+ <Field label="Key">
190
+ <input
191
+ className="w-full rounded-md border border-border bg-transparent px-2 py-1 font-mono text-[12px] outline-none focus:border-accent"
192
+ value={derivedKey}
193
+ onChange={(event) => {
194
+ setKey(event.target.value)
195
+ setKeyEdited(true)
196
+ }}
197
+ placeholder="lowercase_snake_case"
198
+ pattern="^[a-z][a-z0-9_]*$"
199
+ required
200
+ />
201
+ </Field>
202
+ <Field label="Type">
203
+ <select
204
+ className="w-full rounded-md border border-border bg-transparent px-2 py-1 text-[13px] outline-none focus:border-accent"
205
+ value={type}
206
+ onChange={(event) => {
207
+ setType(event.target.value as CustomFieldType)
208
+ }}
209
+ >
210
+ {CUSTOM_FIELD_TYPES.map((option) => (
211
+ <option key={option} value={option}>
212
+ {CUSTOM_FIELD_TYPE_LABELS[option]}
213
+ </option>
214
+ ))}
215
+ </select>
216
+ </Field>
217
+ {OPTIONS_TYPES.has(type) && (
218
+ <Field label="Options (one per line, or comma-separated)">
219
+ <textarea
220
+ className="min-h-[80px] w-full rounded-md border border-border bg-transparent px-2 py-1 text-[13px] outline-none focus:border-accent"
221
+ value={optionsText}
222
+ onChange={(event) => {
223
+ setOptionsText(event.target.value)
224
+ }}
225
+ required
226
+ />
227
+ </Field>
228
+ )}
229
+ </div>
230
+ <Field label="Description (agent-facing help text)">
231
+ <textarea
232
+ className="min-h-[60px] w-full rounded-md border border-border bg-transparent px-2 py-1 text-[13px] outline-none focus:border-accent"
233
+ value={description}
234
+ onChange={(event) => {
235
+ setDescription(event.target.value)
236
+ }}
237
+ />
238
+ </Field>
239
+
240
+ {create.error !== null && <ErrorPanel error={create.error} />}
241
+
242
+ <div className="flex justify-end gap-2">
243
+ <button
244
+ type="button"
245
+ onClick={() => {
246
+ onDone()
247
+ }}
248
+ className="rounded-md border border-border px-3 py-1 text-[12px] text-ink-muted hover:text-ink"
249
+ >
250
+ Cancel
251
+ </button>
252
+ <button
253
+ type="submit"
254
+ disabled={create.isPending}
255
+ className="rounded-md bg-accent px-3 py-1 text-[12px] font-medium text-white hover:bg-accent-hover disabled:opacity-50"
256
+ >
257
+ {create.isPending ? 'Adding…' : 'Add field'}
258
+ </button>
259
+ </div>
260
+ </form>
261
+ )
262
+ }
263
+
264
+ interface FieldRowProps {
265
+ readonly definition: CustomFieldDefinition
266
+ }
267
+
268
+ function FieldRow({ definition }: FieldRowProps): React.JSX.Element {
269
+ const update = useUpdateCustomField()
270
+ const remove = useDeleteCustomField()
271
+ const [editing, setEditing] = useState(false)
272
+ const [label, setLabel] = useState(definition.label)
273
+ const [description, setDescription] = useState(definition.description)
274
+ const [optionsText, setOptionsText] = useState(definition.options.join('\n'))
275
+ const [confirmingDelete, setConfirmingDelete] = useState(false)
276
+
277
+ const hasOptions = OPTIONS_TYPES.has(definition.type as CustomFieldType)
278
+
279
+ function save(event: FormEvent): void {
280
+ event.preventDefault()
281
+ const options = hasOptions
282
+ ? optionsText
283
+ .split(/[\n,]/u)
284
+ .map((option) => option.trim())
285
+ .filter((option) => option.length > 0)
286
+ : undefined
287
+ update.runAsync({
288
+ id: definition.id,
289
+ changes: {
290
+ label: label.trim(),
291
+ description: description.trim(),
292
+ ...(options === undefined ? {} : { options }),
293
+ },
294
+ })
295
+ .then(() => {
296
+ setEditing(false)
297
+ })
298
+ .catch(() => {
299
+ // Error rendered inline.
300
+ })
301
+ }
302
+
303
+ return (
304
+ <div className="rounded-md border border-border p-3">
305
+ <div className="flex items-start justify-between gap-3">
306
+ <div className="min-w-0 flex-1">
307
+ <div className="flex flex-wrap items-baseline gap-2">
308
+ <div className="text-[14px] font-semibold text-ink">{definition.label}</div>
309
+ <code className="rounded bg-surface-raised px-1.5 py-0.5 font-mono text-[11px] text-ink-muted">
310
+ {definition.key}
311
+ </code>
312
+ <span className="text-[11px] text-ink-muted">
313
+ {CUSTOM_FIELD_TYPE_LABELS[definition.type as CustomFieldType]}
314
+ </span>
315
+ </div>
316
+ {definition.description.length > 0 && (
317
+ <p className="mt-1 text-[12px] text-ink-muted">{definition.description}</p>
318
+ )}
319
+ {hasOptions && (
320
+ <p className="mt-1 text-[12px] text-ink-muted">
321
+ Options: {definition.options.join(', ')}
322
+ </p>
323
+ )}
324
+ </div>
325
+ <div className="flex shrink-0 gap-2">
326
+ <button
327
+ type="button"
328
+ onClick={() => {
329
+ setEditing((current) => !current)
330
+ }}
331
+ className="text-[12px] text-ink-muted hover:text-ink"
332
+ >
333
+ {editing ? 'Cancel' : 'Edit'}
334
+ </button>
335
+ <button
336
+ type="button"
337
+ onClick={() => {
338
+ setConfirmingDelete(true)
339
+ }}
340
+ className="text-[12px] text-red-600 hover:text-red-700"
341
+ >
342
+ Delete
343
+ </button>
344
+ </div>
345
+ </div>
346
+
347
+ {editing && (
348
+ <form onSubmit={save} className="mt-3 space-y-2">
349
+ <Field label="Label">
350
+ <input
351
+ className="w-full rounded-md border border-border bg-transparent px-2 py-1 text-[13px] outline-none focus:border-accent"
352
+ value={label}
353
+ onChange={(event) => {
354
+ setLabel(event.target.value)
355
+ }}
356
+ required
357
+ />
358
+ </Field>
359
+ {hasOptions && (
360
+ <Field label="Options">
361
+ <textarea
362
+ className="min-h-[80px] w-full rounded-md border border-border bg-transparent px-2 py-1 text-[13px] outline-none focus:border-accent"
363
+ value={optionsText}
364
+ onChange={(event) => {
365
+ setOptionsText(event.target.value)
366
+ }}
367
+ />
368
+ </Field>
369
+ )}
370
+ <Field label="Description">
371
+ <textarea
372
+ className="min-h-[60px] w-full rounded-md border border-border bg-transparent px-2 py-1 text-[13px] outline-none focus:border-accent"
373
+ value={description}
374
+ onChange={(event) => {
375
+ setDescription(event.target.value)
376
+ }}
377
+ />
378
+ </Field>
379
+ {update.error !== null && <ErrorPanel error={update.error} />}
380
+ <div className="flex justify-end">
381
+ <button
382
+ type="submit"
383
+ disabled={update.isPending}
384
+ className="rounded-md bg-accent px-3 py-1 text-[12px] font-medium text-white hover:bg-accent-hover disabled:opacity-50"
385
+ >
386
+ {update.isPending ? 'Saving…' : 'Save'}
387
+ </button>
388
+ </div>
389
+ </form>
390
+ )}
391
+
392
+ {confirmingDelete && (
393
+ <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">
394
+ <p>
395
+ Delete the {definition.label} field? This removes its stored values from every{' '}
396
+ {CUSTOM_FIELD_OBJECT_TYPE_LABELS[
397
+ definition.objectType as CustomFieldObjectType
398
+ ].toLowerCase()}{' '}
399
+ record.
400
+ </p>
401
+ {remove.error !== null && <ErrorPanel error={remove.error} />}
402
+ <div className="mt-2 flex justify-end gap-2">
403
+ <button
404
+ type="button"
405
+ onClick={() => {
406
+ setConfirmingDelete(false)
407
+ }}
408
+ 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"
409
+ >
410
+ Cancel
411
+ </button>
412
+ <button
413
+ type="button"
414
+ onClick={() => {
415
+ remove.runAsync(definition.id)
416
+ .then(() => {
417
+ setConfirmingDelete(false)
418
+ })
419
+ .catch(() => {
420
+ // Error rendered inline.
421
+ })
422
+ }}
423
+ disabled={remove.isPending}
424
+ className="rounded-md bg-red-600 px-3 py-1 text-[12px] font-medium text-white hover:bg-red-700 disabled:opacity-50"
425
+ >
426
+ {remove.isPending ? 'Deleting…' : 'Delete'}
427
+ </button>
428
+ </div>
429
+ </div>
430
+ )}
431
+ </div>
432
+ )
433
+ }
434
+
435
+ interface FieldProps {
436
+ readonly label: string
437
+ readonly children: React.ReactNode
438
+ }
439
+
440
+ function Field({ label, children }: FieldProps): React.JSX.Element {
441
+ return (
442
+ <label className="block text-[12px]">
443
+ <span className="mb-1 block font-medium text-ink-muted">{label}</span>
444
+ {children}
445
+ </label>
446
+ )
447
+ }
448
+
449
+ function slugify(label: string): string {
450
+ return label
451
+ .toLowerCase()
452
+ .replace(/[^a-z0-9]+/gu, '_')
453
+ .replace(/^_+|_+$/gu, '')
454
+ .replace(/^([0-9])/u, '_$1')
455
+ }