@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
@@ -35,10 +35,12 @@ export interface RoleFilters {
35
35
  /** Roles in any of these statuses. Repeats on the wire. */
36
36
  readonly statuses?: readonly RoleStatus[] | undefined
37
37
  readonly limit?: number | undefined
38
+ /** `field` ascending, `-field` descending. Only `title`, `created_at`, `updated_at` are sortable. */
39
+ readonly sort?: string | undefined
38
40
  }
39
41
 
40
42
  function roleQuery(filters: RoleFilters): QueryParameters {
41
- return { q: filters.term, status: filters.statuses, limit: filters.limit }
43
+ return { q: filters.term, status: filters.statuses, limit: filters.limit, sort: filters.sort }
42
44
  }
43
45
 
44
46
  export function useRoles(
@@ -0,0 +1,57 @@
1
+ import { sampleDataCountsSchema } from '@kelpie/schemas'
2
+ import type { SampleDataCounts } from '@kelpie/schemas'
3
+ import { useMutation, useQueryClient } from '@tanstack/react-query'
4
+
5
+ import { useApiClient } from '../context.ts'
6
+ import type { MutationResult } from '../resource.ts'
7
+ import { asMutationResult } from './mutation.ts'
8
+
9
+ /**
10
+ * `POST /v1/workspaces/:id/sample-data`: install the sample workspace.
11
+ *
12
+ * The workspace id arrives as a mutation argument rather than being read from
13
+ * the session. That lets the onboarding wizard install into the workspace it
14
+ * just created, before the session cache catches up with it.
15
+ *
16
+ * On success, every CRM list is invalidated because the rows this just wrote
17
+ * are in all of them.
18
+ */
19
+
20
+ export interface InstallSampleDataInput {
21
+ readonly workspaceId: string
22
+ }
23
+
24
+ export function useInstallSampleData(): MutationResult<InstallSampleDataInput, SampleDataCounts> {
25
+ const client = useApiClient()
26
+ const cache = useQueryClient()
27
+ const mutation = useMutation({
28
+ mutationFn: ({ workspaceId }: InstallSampleDataInput) =>
29
+ client.post(
30
+ `/workspaces/${workspaceId}/sample-data`,
31
+ {},
32
+ sampleDataCountsSchema.parse,
33
+ ),
34
+ onSuccess: async () => {
35
+ await Promise.all(
36
+ [
37
+ 'people',
38
+ 'companies',
39
+ 'positions',
40
+ 'deals',
41
+ 'plans',
42
+ 'notes',
43
+ 'activities',
44
+ 'decisions',
45
+ 'opportunities',
46
+ 'raises',
47
+ 'partnerships',
48
+ 'enquiries',
49
+ 'roles',
50
+ 'candidates',
51
+ ].map((name) => cache.invalidateQueries({ queryKey: [name] })),
52
+ )
53
+ },
54
+ })
55
+
56
+ return asMutationResult(mutation)
57
+ }
@@ -14,12 +14,11 @@ import type {
14
14
  WebhookDelivery,
15
15
  WebhookInput,
16
16
  } from '@kelpie/schemas'
17
- import { keepPreviousData, useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query'
17
+ import { useMutation, useQueryClient } from '@tanstack/react-query'
18
18
 
19
19
  import type { QueryParameters } from '../client.ts'
20
20
  import { useApiClient } from '../context.ts'
21
- import { toError } from '../errors.ts'
22
- import { createResourceHooks } from '../resource.ts'
21
+ import { createResourceHooks, usePagedList } from '../resource.ts'
23
22
  import type { MutationResult, RecordListResult, UpdateArguments } from '../resource.ts'
24
23
  import { asMutationResult } from './mutation.ts'
25
24
 
@@ -79,29 +78,14 @@ export function useWebhookDeliveries(
79
78
  webhookId: string,
80
79
  filters: WebhookDeliveryFilters = {},
81
80
  ): RecordListResult<WebhookDelivery> {
82
- const client = useApiClient()
83
- const result = useInfiniteQuery({
81
+ const query: QueryParameters = { status: filters.status }
82
+
83
+ return usePagedList<WebhookDelivery>({
84
84
  queryKey: ['webhooks', 'deliveries', webhookId, filters.status ?? 'all'],
85
- queryFn: ({ pageParam }) =>
86
- client.list(`/webhooks/${webhookId}/deliveries`, webhookDeliverySchema.parse, {
87
- status: filters.status,
88
- ...(pageParam === null ? {} : { cursor: pageParam }),
89
- }),
90
- initialPageParam: null as string | null,
91
- getNextPageParam: (lastPage) => lastPage.nextCursor,
92
- placeholderData: keepPreviousData,
85
+ path: `/webhooks/${webhookId}/deliveries`,
86
+ decode: webhookDeliverySchema.parse,
87
+ query,
93
88
  })
94
-
95
- return {
96
- records: result.data?.pages.flatMap((page) => page.items) ?? [],
97
- isLoading: result.isPending,
98
- error: toError(result.error),
99
- hasMore: result.hasNextPage,
100
- isLoadingMore: result.isFetchingNextPage,
101
- loadMore: () => {
102
- void result.fetchNextPage()
103
- },
104
- }
105
89
  }
106
90
 
107
91
  /**
@@ -11,10 +11,16 @@ import { DealsPage } from '../pages/DealsPage.tsx'
11
11
  import { DecisionsPage } from '../pages/DecisionsPage.tsx'
12
12
  import { SearchPage } from '../pages/SearchPage.tsx'
13
13
  import { FormDetail } from '../pages/FormDetail.tsx'
14
+ import { SubmissionDetail } from '../pages/forms/SubmissionDetail.tsx'
14
15
  import { FormsPage } from '../pages/FormsPage.tsx'
15
16
  import { FundraisingPage } from '../pages/FundraisingPage.tsx'
16
17
  import { HiringPage } from '../pages/HiringPage.tsx'
18
+ import { ListDetail } from '../pages/ListDetail.tsx'
19
+ import { ListsPage } from '../pages/ListsPage.tsx'
17
20
  import { NotFoundPage } from '../pages/NotFoundPage.tsx'
21
+ import { EnquiriesPage } from '../pages/EnquiriesPage.tsx'
22
+ import { EnquiryDetail } from '../pages/EnquiryDetail.tsx'
23
+ import { EnquiryStageSettingsPage } from '../pages/EnquiryStageSettingsPage.tsx'
18
24
  import { OpportunitiesPage } from '../pages/OpportunitiesPage.tsx'
19
25
  import { OpportunityDetail } from '../pages/OpportunityDetail.tsx'
20
26
  import { OpportunityStageSettingsPage } from '../pages/OpportunityStageSettingsPage.tsx'
@@ -34,6 +40,8 @@ import { ProfilePage } from '../pages/account/ProfilePage.tsx'
34
40
  import { SecurityPage } from '../pages/account/SecurityPage.tsx'
35
41
  import { ApiKeysPage } from '../pages/admin/ApiKeysPage.tsx'
36
42
  import { DataPage } from '../pages/admin/DataPage.tsx'
43
+ import { FieldsPage } from '../pages/admin/FieldsPage.tsx'
44
+ import { PrivacyPage } from '../pages/admin/PrivacyPage.tsx'
37
45
  import { McpPage } from '../pages/admin/McpPage.tsx'
38
46
  import { ModulesPage } from '../pages/admin/ModulesPage.tsx'
39
47
  import { TeamPage } from '../pages/admin/TeamPage.tsx'
@@ -153,21 +161,29 @@ function AppRoutes(): React.JSX.Element {
153
161
  <Route path="opportunities" element={<OpportunitiesPage />} />
154
162
  <Route path="opportunities/settings" element={<OpportunityStageSettingsPage />} />
155
163
  <Route path="opportunities/:id" element={<OpportunityDetail />} />
164
+ <Route path="enquiries" element={<EnquiriesPage />} />
165
+ <Route path="enquiries/settings" element={<EnquiryStageSettingsPage />} />
166
+ <Route path="enquiries/:id" element={<EnquiryDetail />} />
156
167
  <Route path="fundraising" element={<FundraisingPage />} />
157
168
  <Route path="fundraising/settings" element={<RaiseStageSettingsPage />} />
158
169
  <Route path="fundraising/:id" element={<RaiseDetail />} />
159
170
  <Route path="partnerships" element={<PartnershipsPage />} />
160
171
  <Route path="partnerships/settings" element={<PartnershipStageSettingsPage />} />
161
172
  <Route path="partnerships/:id" element={<PartnershipDetail />} />
173
+ <Route path="lists" element={<ListsPage />} />
174
+ <Route path="lists/:id" element={<ListDetail />} />
162
175
  <Route path="planning" element={<PlanningPage />} />
163
176
  <Route path="decisions" element={<DecisionsPage />} />
164
177
  <Route path="forms" element={<FormsPage />} />
178
+ <Route path="forms/:id/submissions/:submissionId" element={<SubmissionDetail />} />
165
179
  <Route path="forms/:id" element={<FormDetail />} />
166
180
  {/* The optional segment is the mockup's: /handbook opens the first page. */}
167
181
  <Route path="handbook" element={<HandbookLayout />} />
168
182
  <Route path="handbook/:pageId" element={<HandbookLayout />} />
169
183
  <Route path="admin/workspace" element={<WorkspaceSettingsPage />} />
170
184
  <Route path="admin/team" element={<TeamPage />} />
185
+ <Route path="admin/fields" element={<FieldsPage />} />
186
+ <Route path="admin/privacy" element={<PrivacyPage />} />
171
187
  <Route path="admin/data" element={<DataPage />} />
172
188
  <Route path="admin/api-keys" element={<ApiKeysPage />} />
173
189
  <Route path="admin/mcp" element={<McpPage />} />
@@ -4,6 +4,7 @@ import { useTimezone } from '../api/resources/account.ts'
4
4
  import { useActivities } from '../api/resources/activities.ts'
5
5
  import { useMembers } from '../api/resources/members.ts'
6
6
  import { formatRelativeTime, monthLabel } from '../lib/dates.ts'
7
+ import { Paginator } from './Paginator.tsx'
7
8
  import { ErrorPanel } from './QueryState.tsx'
8
9
  import { SectionHeader } from './SectionHeader.tsx'
9
10
 
@@ -47,6 +48,7 @@ const TARGET_TYPE_LABELS: Readonly<Record<RecordTargetType, string>> = {
47
48
  opportunity: 'Opportunity',
48
49
  partnership: 'Partnership',
49
50
  raise: 'Raise',
51
+ enquiry: 'Enquiry',
50
52
  candidate: 'Candidate',
51
53
  }
52
54
 
@@ -118,16 +120,7 @@ export function ActivitiesPanel({
118
120
  ))}
119
121
  </div>
120
122
 
121
- {activities.hasMore && (
122
- <button
123
- type="button"
124
- onClick={activities.loadMore}
125
- disabled={activities.isLoadingMore}
126
- className="mt-3 rounded-md border border-border px-3 py-1.5 text-[12px] font-medium text-ink-muted transition hover:border-border-strong hover:text-ink"
127
- >
128
- {activities.isLoadingMore ? 'Loading…' : 'Load more'}
129
- </button>
130
- )}
123
+ <Paginator list={activities} />
131
124
  </section>
132
125
  )
133
126
  }
@@ -0,0 +1,124 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+
3
+ /**
4
+ * A "Columns" button that opens a popover of checkboxes for the columns a list
5
+ * page can show.
6
+ *
7
+ * The picker owns nothing but the popover: the selection lives with the caller
8
+ * (usually `useListView`) so the checked state cannot fall out of step with the
9
+ * table beside it. `options` is the full set the page supports, in the display
10
+ * order the header uses.
11
+ *
12
+ * The first column is pinned. A list with no columns at all is not a preference
13
+ * anyone would make on purpose, and the primary column is what makes each row
14
+ * identifiable — losing it turns every row into a set of anonymous fields.
15
+ */
16
+ export interface ColumnPickerOption {
17
+ readonly key: string
18
+ readonly label: string
19
+ }
20
+
21
+ export interface ColumnPickerProps {
22
+ readonly options: readonly ColumnPickerOption[]
23
+ readonly visibleKeys: readonly string[]
24
+ readonly onChange: (keys: readonly string[]) => void
25
+ }
26
+
27
+ export function ColumnPicker({ options, visibleKeys, onChange }: ColumnPickerProps): React.JSX.Element {
28
+ const [open, setOpen] = useState(false)
29
+ const containerRef = useRef<HTMLDivElement>(null)
30
+ const pinnedKey = options[0]?.key
31
+
32
+ useEffect(() => {
33
+ if (!open) {
34
+ return
35
+ }
36
+
37
+ function onPointerDown(event: MouseEvent): void {
38
+ if (containerRef.current?.contains(event.target as Node) !== true) {
39
+ setOpen(false)
40
+ }
41
+ }
42
+
43
+ function onKeyDown(event: KeyboardEvent): void {
44
+ if (event.key === 'Escape') {
45
+ setOpen(false)
46
+ }
47
+ }
48
+
49
+ document.addEventListener('mousedown', onPointerDown)
50
+ document.addEventListener('keydown', onKeyDown)
51
+
52
+ return () => {
53
+ document.removeEventListener('mousedown', onPointerDown)
54
+ document.removeEventListener('keydown', onKeyDown)
55
+ }
56
+ }, [open])
57
+
58
+ function toggle(key: string, checked: boolean): void {
59
+ if (key === pinnedKey) {
60
+ return
61
+ }
62
+
63
+ // Preserve the order the caller passed in: the stored value is the visible
64
+ // set, and re-adding a column should put it back where the table draws it,
65
+ // not append it wherever the click landed.
66
+ const next = options.map((option) => option.key).filter((candidate) => {
67
+ if (candidate === key) {
68
+ return checked
69
+ }
70
+
71
+ return visibleKeys.includes(candidate)
72
+ })
73
+
74
+ onChange(next)
75
+ }
76
+
77
+ return (
78
+ <div className="relative" ref={containerRef}>
79
+ <button
80
+ type="button"
81
+ onClick={() => {
82
+ setOpen((current) => !current)
83
+ }}
84
+ aria-expanded={open}
85
+ aria-haspopup="menu"
86
+ className="rounded-md border border-border bg-surface-raised px-3 py-1.5 text-[12px] font-medium text-ink transition hover:border-border-strong hover:bg-surface-sunken"
87
+ >
88
+ Columns
89
+ </button>
90
+ {open && (
91
+ <div
92
+ role="menu"
93
+ className="absolute right-0 z-20 mt-1.5 max-h-80 w-56 overflow-y-auto rounded-md border border-border bg-surface-raised py-1"
94
+ >
95
+ {options.map((option) => {
96
+ const checked = visibleKeys.includes(option.key)
97
+ const pinned = option.key === pinnedKey
98
+
99
+ return (
100
+ <label
101
+ key={option.key}
102
+ className={[
103
+ 'flex items-center gap-2 px-3 py-1.5 text-[13px] text-ink',
104
+ pinned ? 'cursor-default text-ink-muted' : 'cursor-pointer hover:bg-surface-sunken',
105
+ ].join(' ')}
106
+ >
107
+ <input
108
+ type="checkbox"
109
+ checked={checked}
110
+ disabled={pinned}
111
+ onChange={(event) => {
112
+ toggle(option.key, event.target.checked)
113
+ }}
114
+ className="h-3.5 w-3.5"
115
+ />
116
+ <span>{option.label}</span>
117
+ </label>
118
+ )
119
+ })}
120
+ </div>
121
+ )}
122
+ </div>
123
+ )
124
+ }
@@ -0,0 +1,330 @@
1
+ import { CUSTOM_FIELD_TYPES_WITH_OPTIONS } from '@kelpie/schemas'
2
+ import type {
3
+ CustomFieldDefinition,
4
+ CustomFieldObjectType,
5
+ CustomFieldType,
6
+ CustomFieldValue,
7
+ CustomFieldValues,
8
+ } from '@kelpie/schemas'
9
+ import { useMemo, useState } from 'react'
10
+ import { Link } from 'react-router'
11
+
12
+ import { useCustomFields } from '../api/resources/customFields.ts'
13
+ import { InlineEdit } from './InlineEdit.tsx'
14
+
15
+ /**
16
+ * Renders the workspace's custom fields for one record as a full-width panel,
17
+ * dropped into a detail page's Fields tab.
18
+ *
19
+ * Each field patches only its own key; the server merges the sent object into
20
+ * stored values in the same transaction.
21
+ *
22
+ * Currency is edited in major units in the box and stored as integer cents,
23
+ * mirroring how DealDetail handles `value_cents`. Multi-select uses a compact
24
+ * checkbox list because InlineEdit only handles one string in and out.
25
+ *
26
+ * `useHasCustomFields` lets a caller decide whether to render the tab at all —
27
+ * a workspace with no definitions should not see a Fields tab full of empty
28
+ * space with only a "go set one up" pointer.
29
+ */
30
+
31
+ export interface CustomFieldsPanelProps {
32
+ readonly objectType: CustomFieldObjectType
33
+ readonly values: CustomFieldValues
34
+ readonly onPatch: (change: Readonly<Record<string, CustomFieldValue | null>>) => void
35
+ }
36
+
37
+ const OPTIONS_TYPES: ReadonlySet<CustomFieldType> = new Set(CUSTOM_FIELD_TYPES_WITH_OPTIONS)
38
+
39
+ export function CustomFieldsPanel({
40
+ objectType,
41
+ values,
42
+ onPatch,
43
+ }: CustomFieldsPanelProps): React.JSX.Element {
44
+ const definitions = useCustomFields({ objectType, sort: 'sort_order', limit: 200 })
45
+
46
+ if (definitions.isLoading) {
47
+ return <p className="text-[13px] text-ink-muted">Loading fields…</p>
48
+ }
49
+
50
+ if (definitions.records.length === 0) {
51
+ return (
52
+ <p className="text-[13px] text-ink-muted">
53
+ No custom fields yet. Add them at{' '}
54
+ <Link to="/admin/fields" className="text-accent hover:text-accent-hover">
55
+ Admin → Fields
56
+ </Link>
57
+ .
58
+ </p>
59
+ )
60
+ }
61
+
62
+ return (
63
+ <dl className="grid gap-x-8 gap-y-5 sm:grid-cols-2">
64
+ {definitions.records.map((definition) => (
65
+ <CustomFieldControl
66
+ key={definition.id}
67
+ definition={definition}
68
+ value={values[definition.key]}
69
+ onChange={(next) => {
70
+ onPatch({ [definition.key]: next })
71
+ }}
72
+ />
73
+ ))}
74
+ </dl>
75
+ )
76
+ }
77
+
78
+ interface CustomFieldControlProps {
79
+ readonly definition: CustomFieldDefinition
80
+ readonly value: CustomFieldValue | undefined
81
+ readonly onChange: (value: CustomFieldValue | null) => void
82
+ }
83
+
84
+ function CustomFieldControl({
85
+ definition,
86
+ value,
87
+ onChange,
88
+ }: CustomFieldControlProps): React.JSX.Element {
89
+ const type = definition.type as CustomFieldType
90
+
91
+ return (
92
+ <div>
93
+ <dt className="text-[10px] font-semibold tracking-wide text-ink-faint uppercase">
94
+ {definition.label}
95
+ </dt>
96
+ {definition.description.length > 0 && (
97
+ <p className="mt-0.5 text-[11px] text-ink-muted">{definition.description}</p>
98
+ )}
99
+ <dd className="mt-1 text-[13px] text-ink">
100
+ <ControlFor definition={definition} type={type} value={value} onChange={onChange} />
101
+ </dd>
102
+ </div>
103
+ )
104
+ }
105
+
106
+ interface ControlForProps {
107
+ readonly definition: CustomFieldDefinition
108
+ readonly type: CustomFieldType
109
+ readonly value: CustomFieldValue | undefined
110
+ readonly onChange: (value: CustomFieldValue | null) => void
111
+ }
112
+
113
+ function ControlFor({
114
+ definition,
115
+ type,
116
+ value,
117
+ onChange,
118
+ }: ControlForProps): React.JSX.Element {
119
+ if (type === 'checkbox') {
120
+ return (
121
+ <label className="inline-flex cursor-pointer items-center gap-2">
122
+ <input
123
+ type="checkbox"
124
+ checked={value === true}
125
+ onChange={(event) => {
126
+ onChange(event.target.checked)
127
+ }}
128
+ className="rounded border-border"
129
+ />
130
+ <span>{value === true ? 'Yes' : 'No'}</span>
131
+ </label>
132
+ )
133
+ }
134
+
135
+ if (type === 'multi_select') {
136
+ return (
137
+ <MultiSelectField
138
+ options={definition.options}
139
+ selected={Array.isArray(value) ? (value as readonly string[]) : []}
140
+ onChange={onChange}
141
+ />
142
+ )
143
+ }
144
+
145
+ if (type === 'currency') {
146
+ const currentCents =
147
+ typeof value === 'object' &&
148
+ value !== null &&
149
+ !Array.isArray(value) &&
150
+ 'amountCents' in value
151
+ ? value.amountCents
152
+ : null
153
+ const currentCurrency =
154
+ typeof value === 'object' &&
155
+ value !== null &&
156
+ !Array.isArray(value) &&
157
+ 'currency' in value
158
+ ? value.currency
159
+ : 'USD'
160
+
161
+ return (
162
+ <InlineEdit
163
+ type="number"
164
+ value={currentCents === null ? '' : String(currentCents / 100)}
165
+ onChange={(raw) => {
166
+ const trimmed = raw.trim()
167
+ if (trimmed.length === 0) {
168
+ onChange(null)
169
+ return
170
+ }
171
+ const parsed = Number(trimmed)
172
+ if (!Number.isFinite(parsed)) {
173
+ return
174
+ }
175
+ onChange({
176
+ amountCents: Math.round(parsed * 100),
177
+ currency: currentCurrency,
178
+ })
179
+ }}
180
+ emptyLabel="Add…"
181
+ />
182
+ )
183
+ }
184
+
185
+ if (OPTIONS_TYPES.has(type)) {
186
+ // A stored value that is no longer in the definition's options must still
187
+ // be visible until the reader chooses another; keep it in the list.
188
+ const stored = typeof value === 'string' ? value : ''
189
+ const options = optionsWithStale(definition.options, stored)
190
+ return (
191
+ <InlineEdit
192
+ value={stored}
193
+ options={options.map((option) => ({ value: option, label: option }))}
194
+ onChange={(next) => {
195
+ onChange(next.length === 0 ? null : next)
196
+ }}
197
+ emptyLabel="Choose…"
198
+ />
199
+ )
200
+ }
201
+
202
+ if (type === 'number') {
203
+ const current = typeof value === 'number' ? String(value) : ''
204
+ return (
205
+ <InlineEdit
206
+ type="number"
207
+ value={current}
208
+ onChange={(raw) => {
209
+ const trimmed = raw.trim()
210
+ if (trimmed.length === 0) {
211
+ onChange(null)
212
+ return
213
+ }
214
+ const parsed = Number(trimmed)
215
+ if (Number.isFinite(parsed)) {
216
+ onChange(parsed)
217
+ }
218
+ }}
219
+ emptyLabel="Add…"
220
+ />
221
+ )
222
+ }
223
+
224
+ const inputType: 'text' | 'date' | 'url' | 'email' =
225
+ type === 'date' ? 'date' : type === 'url' ? 'url' : 'text'
226
+ const multiline = type === 'long_text'
227
+ const current = typeof value === 'string' ? value : ''
228
+
229
+ return (
230
+ <InlineEdit
231
+ type={inputType}
232
+ multiline={multiline}
233
+ value={current}
234
+ onChange={(next) => {
235
+ onChange(next.length === 0 ? null : next)
236
+ }}
237
+ emptyLabel="Add…"
238
+ />
239
+ )
240
+ }
241
+
242
+ interface MultiSelectFieldProps {
243
+ readonly options: readonly string[]
244
+ readonly selected: readonly string[]
245
+ readonly onChange: (value: readonly string[] | null) => void
246
+ }
247
+
248
+ function MultiSelectField({
249
+ options,
250
+ selected,
251
+ onChange,
252
+ }: MultiSelectFieldProps): React.JSX.Element {
253
+ const [open, setOpen] = useState(false)
254
+ const allOptions = useMemo(
255
+ () => optionsWithStale(options, ...selected),
256
+ [options, selected],
257
+ )
258
+
259
+ const chips = selected.length === 0 ? null : (
260
+ <div className="flex flex-wrap gap-1">
261
+ {selected.map((entry) => (
262
+ <span
263
+ key={entry}
264
+ className="inline-flex items-center gap-1 rounded-full bg-surface-raised px-2 py-0.5 text-[11px]"
265
+ >
266
+ {entry}
267
+ <button
268
+ type="button"
269
+ onClick={() => {
270
+ const next = selected.filter((existing) => existing !== entry)
271
+ onChange(next.length === 0 ? null : next)
272
+ }}
273
+ className="text-ink-muted hover:text-ink"
274
+ aria-label={`Remove ${entry}`}
275
+ >
276
+ ×
277
+ </button>
278
+ </span>
279
+ ))}
280
+ </div>
281
+ )
282
+
283
+ return (
284
+ <div className="space-y-1">
285
+ {chips ?? <span className="text-ink-faint">None</span>}
286
+ <button
287
+ type="button"
288
+ onClick={() => {
289
+ setOpen((current) => !current)
290
+ }}
291
+ className="text-[12px] text-ink-muted hover:text-ink"
292
+ >
293
+ {open ? 'Close' : 'Edit'}
294
+ </button>
295
+ {open && (
296
+ <ul className="space-y-1 rounded-md border border-border bg-surface-raised p-2">
297
+ {allOptions.map((option) => {
298
+ const chosen = selected.includes(option)
299
+ return (
300
+ <li key={option}>
301
+ <label className="flex cursor-pointer items-center gap-2 text-[12px]">
302
+ <input
303
+ type="checkbox"
304
+ checked={chosen}
305
+ onChange={() => {
306
+ const next = chosen
307
+ ? selected.filter((existing) => existing !== option)
308
+ : [...selected, option]
309
+ onChange(next.length === 0 ? null : next)
310
+ }}
311
+ />
312
+ <span>{option}</span>
313
+ </label>
314
+ </li>
315
+ )
316
+ })}
317
+ </ul>
318
+ )}
319
+ </div>
320
+ )
321
+ }
322
+
323
+ function optionsWithStale(
324
+ options: readonly string[],
325
+ ...stored: readonly string[]
326
+ ): readonly string[] {
327
+ const seen = new Set(options)
328
+ const extras = stored.filter((entry) => entry.length > 0 && !seen.has(entry))
329
+ return extras.length === 0 ? options : [...options, ...extras]
330
+ }