@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
@@ -2,13 +2,19 @@ import type { Form } from '@kelpie/schemas'
2
2
  import { useState } from 'react'
3
3
  import { useNavigate } from 'react-router'
4
4
 
5
+ import { useTimezone } from '../api/resources/account.ts'
5
6
  import { useCreateForm, useForms } from '../api/resources/forms.ts'
6
7
  import { Chip } from '../components/Chip.tsx'
8
+ import { ColumnPicker } from '../components/ColumnPicker.tsx'
7
9
  import { DataTable } from '../components/DataTable.tsx'
8
10
  import type { Column } from '../components/DataTable.tsx'
9
11
  import { FilterBar, PageHeader } from '../components/PageHeader.tsx'
12
+ import { Paginator } from '../components/Paginator.tsx'
10
13
  import { ErrorPanel, LoadingPanel } from '../components/QueryState.tsx'
11
14
  import { SegmentedControl } from '../components/SegmentedControl.tsx'
15
+ import { formatDate } from '../lib/dates.ts'
16
+ import { useListView } from '../lib/listView.ts'
17
+ import { serverSortOnly } from '../lib/sort.ts'
12
18
  import { CONTACT_FORM_FIELDS } from './forms/template.ts'
13
19
 
14
20
  /**
@@ -31,6 +37,10 @@ const STATUS_OPTIONS = [
31
37
  { id: 'paused' as const, label: 'Paused' },
32
38
  ]
33
39
 
40
+ const DEFAULT_VISIBLE_KEYS: readonly string[] = ['name', 'status', 'fields', 'deal']
41
+
42
+ const SERVER_SORT_KEYS: readonly string[] = ['name', 'created_at', 'updated_at']
43
+
34
44
  export function FormsPage(): React.JSX.Element {
35
45
  const [term, setTerm] = useState('')
36
46
  const [status, setStatus] = useState<StatusFilter>('all')
@@ -38,10 +48,11 @@ export function FormsPage(): React.JSX.Element {
38
48
  const navigate = useNavigate()
39
49
  const hasFilter = term.trim().length > 0 || status !== 'all'
40
50
  const createForm = useCreateForm()
51
+ const timezone = useTimezone()
41
52
  const forms = useForms({
42
53
  term: term.trim().length > 0 ? term.trim() : undefined,
43
54
  ...(status === 'all' ? {} : { status }),
44
- sort,
55
+ sort: serverSortOnly(sort, SERVER_SORT_KEYS),
45
56
  })
46
57
 
47
58
  /**
@@ -70,10 +81,17 @@ export function FormsPage(): React.JSX.Element {
70
81
  </>
71
82
  ),
72
83
  },
84
+ {
85
+ key: 'description',
86
+ header: 'Description',
87
+ getSortValue: (form) => form.description,
88
+ render: (form) => form.description ?? '—',
89
+ },
73
90
  {
74
91
  key: 'status',
75
92
  header: 'Status',
76
93
  className: 'w-28',
94
+ getSortValue: (form) => form.status,
77
95
  render: (form) => (
78
96
  <Chip tone={form.status === 'active' ? 'success' : 'neutral'}>{form.status}</Chip>
79
97
  ),
@@ -82,12 +100,14 @@ export function FormsPage(): React.JSX.Element {
82
100
  key: 'fields',
83
101
  header: 'Fields',
84
102
  className: 'w-20',
103
+ getSortValue: (form) => form.fields.length,
85
104
  render: (form) => <span className="font-mono text-[12px]">{form.fields.length}</span>,
86
105
  },
87
106
  {
88
107
  key: 'deal',
89
108
  header: 'Deal',
90
109
  className: 'w-32',
110
+ getSortValue: (form) => form.createDeal,
91
111
  render: (form) =>
92
112
  form.createDeal ? (
93
113
  <Chip tone="accent">Creates deal</Chip>
@@ -95,8 +115,52 @@ export function FormsPage(): React.JSX.Element {
95
115
  <span className="text-ink-faint">—</span>
96
116
  ),
97
117
  },
118
+ {
119
+ key: 'dealNameTemplate',
120
+ header: 'Deal name template',
121
+ getSortValue: (form) => form.dealNameTemplate,
122
+ render: (form) =>
123
+ form.dealNameTemplate === null ? (
124
+ '—'
125
+ ) : (
126
+ <span className="font-mono text-[12px] text-ink-muted">{form.dealNameTemplate}</span>
127
+ ),
128
+ },
129
+ {
130
+ key: 'thankYouMessage',
131
+ header: 'Thank-you',
132
+ getSortValue: (form) => form.thankYouMessage || null,
133
+ render: (form) =>
134
+ form.thankYouMessage.length === 0 ? '—' : (
135
+ <span className="text-ink-muted">{form.thankYouMessage}</span>
136
+ ),
137
+ },
138
+ {
139
+ key: 'publicKey',
140
+ header: 'Public key',
141
+ getSortValue: (form) => form.publicKey,
142
+ render: (form) => (
143
+ <span className="font-mono text-[12px] text-ink-muted">{form.publicKey}</span>
144
+ ),
145
+ },
146
+ {
147
+ key: 'createdAt',
148
+ header: 'Created',
149
+ sortKey: 'created_at',
150
+ render: (form) => formatDate(form.createdAt, timezone),
151
+ },
152
+ {
153
+ key: 'updatedAt',
154
+ header: 'Updated',
155
+ sortKey: 'updated_at',
156
+ render: (form) => formatDate(form.updatedAt, timezone),
157
+ },
98
158
  ]
99
159
 
160
+ const supportedKeys = columns.map((column) => column.key)
161
+ const listView = useListView('forms', supportedKeys, DEFAULT_VISIBLE_KEYS)
162
+ const pickerOptions = columns.map((column) => ({ key: column.key, label: column.header }))
163
+
100
164
  return (
101
165
  <div className="animate-fade-in">
102
166
  <PageHeader
@@ -105,12 +169,19 @@ export function FormsPage(): React.JSX.Element {
105
169
  onAdd={addForm}
106
170
  addLabel="New form"
107
171
  actions={
108
- <SegmentedControl
109
- value={status}
110
- onChange={setStatus}
111
- options={STATUS_OPTIONS}
112
- ariaLabel="Filter by status"
113
- />
172
+ <>
173
+ <SegmentedControl
174
+ value={status}
175
+ onChange={setStatus}
176
+ options={STATUS_OPTIONS}
177
+ ariaLabel="Filter by status"
178
+ />
179
+ <ColumnPicker
180
+ options={pickerOptions}
181
+ visibleKeys={listView.visibleKeys}
182
+ onChange={listView.setVisibleKeys}
183
+ />
184
+ </>
114
185
  }
115
186
  />
116
187
  <FilterBar value={term} onChange={setTerm} placeholder="Filter forms…" />
@@ -123,6 +194,7 @@ export function FormsPage(): React.JSX.Element {
123
194
  <LoadingPanel label="Loading forms…" />
124
195
  ) : (
125
196
  <>
197
+ <Paginator list={forms} placement="top" />
126
198
  <DataTable
127
199
  columns={columns}
128
200
  rows={forms.records}
@@ -133,17 +205,9 @@ export function FormsPage(): React.JSX.Element {
133
205
  emptyAction={hasFilter ? undefined : { label: 'New form', onClick: addForm }}
134
206
  sort={sort}
135
207
  onSortChange={setSort}
208
+ visibleColumnKeys={listView.visibleKeys}
136
209
  />
137
- {forms.hasMore && (
138
- <button
139
- type="button"
140
- onClick={forms.loadMore}
141
- disabled={forms.isLoadingMore}
142
- className="mt-3 rounded-md border border-border px-3 py-1.5 text-[12px] font-medium text-ink transition hover:border-border-strong hover:bg-surface-sunken disabled:opacity-50"
143
- >
144
- {forms.isLoadingMore ? 'Loading…' : 'Load more'}
145
- </button>
146
- )}
210
+ <Paginator list={forms} />
147
211
  </>
148
212
  )}
149
213
  </div>
@@ -1,5 +1,4 @@
1
1
  import type { PipelineStage, Raise } from '@kelpie/schemas'
2
- import { useState } from 'react'
3
2
  import { Link, useNavigate } from 'react-router'
4
3
 
5
4
  import { useCompanies } from '../api/resources/companies.ts'
@@ -7,17 +6,22 @@ import { useMembers } from '../api/resources/members.ts'
7
6
  import { usePipelineStages } from '../api/resources/pipelineStages.ts'
8
7
  import { MAX_PAGE_SIZE, usePlanItems } from '../api/resources/planItems.ts'
9
8
  import { useCreateRaise, useRaises, useUpdateRaise } from '../api/resources/raises.ts'
9
+ import { useTimezone } from '../api/resources/account.ts'
10
10
  import { Chip } from '../components/Chip.tsx'
11
11
  import type { ChipTone } from '../components/Chip.tsx'
12
+ import { ColumnPicker } from '../components/ColumnPicker.tsx'
12
13
  import { DataTable } from '../components/DataTable.tsx'
13
14
  import type { Column, DataTableGroup } from '../components/DataTable.tsx'
14
15
  import { KanbanBoard } from '../components/KanbanBoard.tsx'
15
16
  import { PageHeader } from '../components/PageHeader.tsx'
17
+ import { Paginator } from '../components/Paginator.tsx'
16
18
  import { ErrorPanel, LoadingPanel } from '../components/QueryState.tsx'
17
19
  import { SegmentedControl } from '../components/SegmentedControl.tsx'
18
- import { formatDay } from '../lib/dates.ts'
20
+ import { formatDate, formatDay } from '../lib/dates.ts'
21
+ import { useListView } from '../lib/listView.ts'
19
22
  import { formatMoney } from '../lib/money.ts'
20
23
  import { DUE_BUCKETS, byDateThenTitle, dueBucketFor, nextOpenByTarget } from '../lib/plan.ts'
24
+ import { serverSortOnly } from '../lib/sort.ts'
21
25
 
22
26
  /**
23
27
  * The Fundraising board: one raise per firm per round, grouped by stage. The
@@ -29,6 +33,34 @@ type BoardView = 'list' | 'columns'
29
33
  type PipelineScope = 'open' | 'all'
30
34
  type ListGrouping = 'stage' | 'due'
31
35
 
36
+ const SCOPES: readonly PipelineScope[] = ['open', 'all']
37
+ const GROUPINGS: readonly ListGrouping[] = ['stage', 'due']
38
+
39
+ function readScope(stored: string | undefined): PipelineScope {
40
+ return SCOPES.includes(stored as PipelineScope) ? (stored as PipelineScope) : 'open'
41
+ }
42
+
43
+ function readGrouping(stored: string | undefined): ListGrouping {
44
+ return GROUPINGS.includes(stored as ListGrouping) ? (stored as ListGrouping) : 'stage'
45
+ }
46
+
47
+ const COLUMN_KEYS: readonly string[] = [
48
+ 'name',
49
+ 'company',
50
+ 'stage',
51
+ 'check',
52
+ 'currency',
53
+ 'thesisFit',
54
+ 'passReason',
55
+ 'nextPlan',
56
+ 'owner',
57
+ 'close',
58
+ 'tags',
59
+ 'summary',
60
+ 'createdAt',
61
+ 'updatedAt',
62
+ ]
63
+
32
64
  /** Tones keyed by the seeded slugs. A renamed stage keeps its slug and its tone. */
33
65
  const STAGE_TONES: Readonly<Record<string, ChipTone>> = {
34
66
  closed: 'success',
@@ -43,19 +75,33 @@ function stageTone(stage: PipelineStage): ChipTone {
43
75
  return STAGE_TONES[stage.slug] ?? 'neutral'
44
76
  }
45
77
 
78
+ const DEFAULT_VISIBLE_KEYS: readonly string[] = [
79
+ 'name',
80
+ 'company',
81
+ 'check',
82
+ 'nextPlan',
83
+ 'owner',
84
+ 'close',
85
+ ]
86
+
87
+ const SERVER_SORT_KEYS: readonly string[] = ['name', 'created_at', 'updated_at']
88
+
46
89
  export function FundraisingPage(): React.JSX.Element {
47
90
  const navigate = useNavigate()
48
- const [view, setView] = useState<BoardView>('list')
49
- const [scope, setScope] = useState<PipelineScope>('open')
50
- const [grouping, setGrouping] = useState<ListGrouping>('stage')
51
- const [sort, setSort] = useState<string | undefined>(undefined)
91
+
92
+ const listView = useListView('raises', COLUMN_KEYS, DEFAULT_VISIBLE_KEYS)
93
+ const view: BoardView = listView.mode ?? 'columns'
94
+ const scope = readScope(listView.scope)
95
+ const grouping = readGrouping(listView.grouping)
96
+ const sort = listView.sort
52
97
 
53
98
  const stages = usePipelineStages('raise')
54
- const raises = useRaises({ sort })
99
+ const raises = useRaises({ sort: serverSortOnly(sort, SERVER_SORT_KEYS) })
55
100
  const companies = useCompanies({ limit: 200 })
56
101
  const members = useMembers()
57
102
  const createRaise = useCreateRaise()
58
103
  const updateRaise = useUpdateRaise()
104
+ const timezone = useTimezone()
59
105
 
60
106
  const allStages = [...stages.records].sort((a, b) => a.sortOrder - b.sortOrder)
61
107
  const visibleStages = scope === 'open' ? allStages.filter((stage) => stage.open) : allStages
@@ -76,7 +122,7 @@ export function FundraisingPage(): React.JSX.Element {
76
122
  { enabled: askedIds.length > 0 },
77
123
  )
78
124
  const nextPlanByRaise = nextOpenByTarget(planItems.records)
79
- const plansTruncated = visibleRaises.length > MAX_PAGE_SIZE || planItems.hasMore
125
+ const plansTruncated = visibleRaises.length > MAX_PAGE_SIZE || planItems.hasNext
80
126
 
81
127
  const companyNameById = new Map(companies.records.map((company) => [company.id, company.name]))
82
128
 
@@ -95,6 +141,8 @@ export function FundraisingPage(): React.JSX.Element {
95
141
  await navigate(`/fundraising/${raise.id}`)
96
142
  }
97
143
 
144
+ const stageLabelById = new Map(allStages.map((stage) => [stage.id, stage.label]))
145
+
98
146
  const columns: readonly Column<Raise>[] = [
99
147
  {
100
148
  key: 'name',
@@ -105,12 +153,20 @@ export function FundraisingPage(): React.JSX.Element {
105
153
  {
106
154
  key: 'company',
107
155
  header: 'Firm',
156
+ getSortValue: (raise) => companyNameById.get(raise.companyId) ?? null,
108
157
  render: (raise) => companyNameById.get(raise.companyId) ?? '—',
109
158
  },
159
+ {
160
+ key: 'stage',
161
+ header: 'Stage',
162
+ getSortValue: (raise) => stageLabelById.get(raise.stageId) ?? null,
163
+ render: (raise) => stageLabelById.get(raise.stageId) ?? '—',
164
+ },
110
165
  {
111
166
  key: 'check',
112
167
  header: 'Check',
113
168
  className: 'w-28',
169
+ getSortValue: (raise) => raise.checkSizeCents,
114
170
  render: (raise) =>
115
171
  raise.checkSizeCents === null ? (
116
172
  '—'
@@ -120,9 +176,34 @@ export function FundraisingPage(): React.JSX.Element {
120
176
  </span>
121
177
  ),
122
178
  },
179
+ {
180
+ key: 'currency',
181
+ header: 'Currency',
182
+ getSortValue: (raise) => raise.currency,
183
+ render: (raise) => raise.currency ?? '—',
184
+ },
185
+ {
186
+ key: 'thesisFit',
187
+ header: 'Thesis fit',
188
+ getSortValue: (raise) => raise.thesisFit || null,
189
+ render: (raise) =>
190
+ raise.thesisFit.length === 0 ? '—' : (
191
+ <span className="text-ink-muted">{raise.thesisFit}</span>
192
+ ),
193
+ },
194
+ {
195
+ key: 'passReason',
196
+ header: 'Pass reason',
197
+ getSortValue: (raise) => raise.passReason,
198
+ render: (raise) =>
199
+ raise.passReason === null ? '—' : (
200
+ <span className="text-ink-muted">{raise.passReason}</span>
201
+ ),
202
+ },
123
203
  {
124
204
  key: 'nextPlan',
125
205
  header: 'Next plan',
206
+ getSortValue: (raise) => nextPlanByRaise.get(raise.id)?.date ?? null,
126
207
  render: (raise) => {
127
208
  const next = nextPlanByRaise.get(raise.id)
128
209
 
@@ -143,6 +224,8 @@ export function FundraisingPage(): React.JSX.Element {
143
224
  {
144
225
  key: 'owner',
145
226
  header: 'Owner',
227
+ getSortValue: (raise) =>
228
+ raise.ownerId === null ? null : (members.nameById.get(raise.ownerId) ?? 'Unknown'),
146
229
  render: (raise) =>
147
230
  raise.ownerId === null ? '—' : (members.nameById.get(raise.ownerId) ?? 'Unknown'),
148
231
  },
@@ -150,14 +233,51 @@ export function FundraisingPage(): React.JSX.Element {
150
233
  key: 'close',
151
234
  header: 'Close',
152
235
  className: 'w-28',
236
+ getSortValue: (raise) => raise.expectedClose,
153
237
  render: (raise) => (
154
238
  <span className="font-mono text-[12px] text-ink-muted">
155
239
  {raise.expectedClose === null ? '—' : formatDay(raise.expectedClose)}
156
240
  </span>
157
241
  ),
158
242
  },
243
+ {
244
+ key: 'tags',
245
+ header: 'Tags',
246
+ getSortValue: (raise) => raise.tags.join(', ') || null,
247
+ render: (raise) =>
248
+ raise.tags.length === 0 ? '—' : (
249
+ <span className="flex flex-wrap gap-1">
250
+ {raise.tags.map((tag) => (
251
+ <Chip key={tag}>
252
+ <span className="text-[10px]">{tag}</span>
253
+ </Chip>
254
+ ))}
255
+ </span>
256
+ ),
257
+ },
258
+ {
259
+ key: 'summary',
260
+ header: 'Summary',
261
+ getSortValue: (raise) => raise.summary || null,
262
+ render: (raise) =>
263
+ raise.summary.length === 0 ? '—' : <span className="text-ink-muted">{raise.summary}</span>,
264
+ },
265
+ {
266
+ key: 'createdAt',
267
+ header: 'Created',
268
+ sortKey: 'created_at',
269
+ render: (raise) => formatDate(raise.createdAt, timezone),
270
+ },
271
+ {
272
+ key: 'updatedAt',
273
+ header: 'Updated',
274
+ sortKey: 'updated_at',
275
+ render: (raise) => formatDate(raise.updatedAt, timezone),
276
+ },
159
277
  ]
160
278
 
279
+ const pickerOptions = columns.map((column) => ({ key: column.key, label: column.header }))
280
+
161
281
  const stageGroups: readonly DataTableGroup<Raise>[] = visibleStages.map((stage) => ({
162
282
  id: stage.id,
163
283
  label: <Chip tone={stageTone(stage)}>{stage.label}</Chip>,
@@ -221,7 +341,9 @@ export function FundraisingPage(): React.JSX.Element {
221
341
  <SegmentedControl
222
342
  ariaLabel="Stage scope"
223
343
  value={scope}
224
- onChange={setScope}
344
+ onChange={(next) => {
345
+ listView.setScope(next)
346
+ }}
225
347
  options={[
226
348
  { id: 'open', label: 'Open' },
227
349
  { id: 'all', label: 'All' },
@@ -231,7 +353,9 @@ export function FundraisingPage(): React.JSX.Element {
231
353
  <SegmentedControl
232
354
  ariaLabel="Group the list"
233
355
  value={grouping}
234
- onChange={setGrouping}
356
+ onChange={(next) => {
357
+ listView.setGrouping(next)
358
+ }}
235
359
  options={[
236
360
  { id: 'stage', label: 'By stage' },
237
361
  { id: 'due', label: 'By due' },
@@ -241,12 +365,21 @@ export function FundraisingPage(): React.JSX.Element {
241
365
  <SegmentedControl
242
366
  ariaLabel="Board or list"
243
367
  value={view}
244
- onChange={setView}
368
+ onChange={(next) => {
369
+ listView.setMode(next)
370
+ }}
245
371
  options={[
246
372
  { id: 'list', label: 'List' },
247
373
  { id: 'columns', label: 'Board' },
248
374
  ]}
249
375
  />
376
+ {view === 'list' && (
377
+ <ColumnPicker
378
+ options={pickerOptions}
379
+ visibleKeys={listView.visibleKeys}
380
+ onChange={listView.setVisibleKeys}
381
+ />
382
+ )}
250
383
  </>
251
384
  }
252
385
  />
@@ -269,6 +402,7 @@ export function FundraisingPage(): React.JSX.Element {
269
402
  <LoadingPanel label="Loading raises…" />
270
403
  ) : (
271
404
  <>
405
+ {view === 'list' && <Paginator list={raises} placement="top" />}
272
406
  {view === 'columns' ? (
273
407
  <KanbanBoard
274
408
  stages={visibleStages.map((stage) => ({ id: stage.id, label: stage.label }))}
@@ -313,13 +447,16 @@ export function FundraisingPage(): React.JSX.Element {
313
447
  ? {
314
448
  label: 'Show all raises',
315
449
  onClick: () => {
316
- setScope('all')
450
+ listView.setScope('all')
317
451
  },
318
452
  }
319
453
  : undefined
320
454
  }
321
455
  sort={sort}
322
- onSortChange={setSort}
456
+ onSortChange={(next) => {
457
+ listView.setSort(next)
458
+ }}
459
+ visibleColumnKeys={listView.visibleKeys}
323
460
  />
324
461
  )}
325
462
  {updateRaise.error !== null && (
@@ -327,17 +464,8 @@ export function FundraisingPage(): React.JSX.Element {
327
464
  <ErrorPanel error={updateRaise.error} />
328
465
  </div>
329
466
  )}
330
- {raises.hasMore && (
331
- <button
332
- type="button"
333
- onClick={raises.loadMore}
334
- disabled={raises.isLoadingMore}
335
- className="mt-3 rounded-md border border-border px-3 py-1.5 text-[12px] font-medium text-ink transition hover:border-border-strong hover:bg-surface-sunken disabled:opacity-50"
336
- >
337
- {raises.isLoadingMore ? 'Loading…' : 'Load more'}
338
- </button>
339
- )}
340
- {companies.hasMore && (
467
+ {view === 'list' && <Paginator list={raises} />}
468
+ {companies.hasNext && (
341
469
  <p className="mt-2 text-[11px] text-ink-faint">
342
470
  More companies exist than one page returns, so some firm names may show as “—”.
343
471
  </p>
@@ -6,13 +6,21 @@ import { useNavigate } from 'react-router'
6
6
  import { useTimezone } from '../api/resources/account.ts'
7
7
  import { useCreateRole, useRoles } from '../api/resources/roles.ts'
8
8
  import { Chip } from '../components/Chip.tsx'
9
+ import { ColumnPicker } from '../components/ColumnPicker.tsx'
9
10
  import { DataTable } from '../components/DataTable.tsx'
10
11
  import type { Column } from '../components/DataTable.tsx'
11
12
  import { FilterBar, PageHeader } from '../components/PageHeader.tsx'
13
+ import { Paginator } from '../components/Paginator.tsx'
12
14
  import { ErrorPanel, LoadingPanel } from '../components/QueryState.tsx'
13
15
  import { formatDate } from '../lib/dates.ts'
16
+ import { useListView } from '../lib/listView.ts'
17
+ import { serverSortOnly } from '../lib/sort.ts'
14
18
  import { useRoleCandidateCounts } from './hiringDirectory.ts'
15
19
 
20
+ const DEFAULT_VISIBLE_KEYS: readonly string[] = ['title', 'status', 'candidates', 'created']
21
+
22
+ const SERVER_SORT_KEYS: readonly string[] = ['title', 'created_at', 'updated_at']
23
+
16
24
  /**
17
25
  * The Hiring list: every opening, open or closed.
18
26
  *
@@ -24,8 +32,12 @@ export function HiringPage(): React.JSX.Element {
24
32
  const [term, setTerm] = useState('')
25
33
  const [adding, setAdding] = useState(false)
26
34
  const [title, setTitle] = useState('')
35
+ const [sort, setSort] = useState<string | undefined>(undefined)
27
36
  const navigate = useNavigate()
28
- const roles = useRoles({ term: term.trim().length > 0 ? term.trim() : undefined })
37
+ const roles = useRoles({
38
+ term: term.trim().length > 0 ? term.trim() : undefined,
39
+ sort: serverSortOnly(sort, SERVER_SORT_KEYS),
40
+ })
29
41
  const counts = useRoleCandidateCounts(roles.records.map((role) => role.id))
30
42
  const createRole = useCreateRole()
31
43
  const timezone = useTimezone()
@@ -54,11 +66,13 @@ export function HiringPage(): React.JSX.Element {
54
66
  {
55
67
  key: 'title',
56
68
  header: 'Role',
69
+ sortKey: 'title',
57
70
  render: (role) => <span className="font-medium text-ink">{role.title}</span>,
58
71
  },
59
72
  {
60
73
  key: 'status',
61
74
  header: 'Status',
75
+ getSortValue: (role) => role.status,
62
76
  render: (role) => (
63
77
  <Chip tone={role.status === 'open' ? 'accent' : 'neutral'}>
64
78
  {role.status === 'open' ? 'Open' : 'Closed'}
@@ -69,6 +83,7 @@ export function HiringPage(): React.JSX.Element {
69
83
  key: 'candidates',
70
84
  header: 'Candidates',
71
85
  className: 'w-28',
86
+ getSortValue: (role) => (counts.isLoading ? null : counts.countFor(role.id)),
72
87
  render: (role) => (
73
88
  <span className="font-mono text-[12px]">
74
89
  {counts.isLoading ? '—' : counts.countFor(role.id)}
@@ -78,10 +93,21 @@ export function HiringPage(): React.JSX.Element {
78
93
  {
79
94
  key: 'created',
80
95
  header: 'Created',
96
+ sortKey: 'created_at',
81
97
  render: (role) => formatDate(role.createdAt, timezone),
82
98
  },
99
+ {
100
+ key: 'updated',
101
+ header: 'Updated',
102
+ sortKey: 'updated_at',
103
+ render: (role) => formatDate(role.updatedAt, timezone),
104
+ },
83
105
  ]
84
106
 
107
+ const supportedKeys = columns.map((column) => column.key)
108
+ const listView = useListView('roles', supportedKeys, DEFAULT_VISIBLE_KEYS)
109
+ const pickerOptions = columns.map((column) => ({ key: column.key, label: column.header }))
110
+
85
111
  return (
86
112
  <div className="animate-fade-in">
87
113
  <PageHeader
@@ -90,6 +116,13 @@ export function HiringPage(): React.JSX.Element {
90
116
  setAdding((current) => !current)
91
117
  }}
92
118
  addLabel="Add role"
119
+ actions={
120
+ <ColumnPicker
121
+ options={pickerOptions}
122
+ visibleKeys={listView.visibleKeys}
123
+ onChange={listView.setVisibleKeys}
124
+ />
125
+ }
93
126
  />
94
127
 
95
128
  {adding && (
@@ -140,6 +173,7 @@ export function HiringPage(): React.JSX.Element {
140
173
  <LoadingPanel label="Loading roles…" />
141
174
  ) : (
142
175
  <>
176
+ <Paginator list={roles} placement="top" />
143
177
  <DataTable
144
178
  columns={columns}
145
179
  rows={roles.records}
@@ -148,22 +182,16 @@ export function HiringPage(): React.JSX.Element {
148
182
  void navigate(`/hiring/${role.id}`)
149
183
  }}
150
184
  emptyMessage="No roles yet"
185
+ sort={sort}
186
+ onSortChange={setSort}
187
+ visibleColumnKeys={listView.visibleKeys}
151
188
  />
152
189
  {!counts.isComplete && !counts.isLoading && (
153
190
  <p className="mt-2 text-[11px] text-ink-faint">
154
191
  These roles hold more candidates than one page returns, so some counts read low.
155
192
  </p>
156
193
  )}
157
- {roles.hasMore && (
158
- <button
159
- type="button"
160
- onClick={roles.loadMore}
161
- disabled={roles.isLoadingMore}
162
- className="mt-3 rounded-md border border-border px-3 py-1.5 text-[12px] font-medium text-ink transition hover:border-border-strong hover:bg-surface-sunken disabled:opacity-50"
163
- >
164
- {roles.isLoadingMore ? 'Loading…' : 'Load more'}
165
- </button>
166
- )}
194
+ <Paginator list={roles} />
167
195
  </>
168
196
  )}
169
197
  </div>