@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,281 @@
1
+ import {
2
+ RECORD_TARGET_TYPE_LABELS,
3
+ RECORD_TARGET_TYPES,
4
+ } from '@kelpie/schemas'
5
+ import type { List, RecordTargetType } from '@kelpie/schemas'
6
+ import { useState } from 'react'
7
+ import type { FormEvent } from 'react'
8
+ import { useNavigate } from 'react-router'
9
+
10
+ import { useTimezone } from '../api/resources/account.ts'
11
+ import { useCreateList, useLists } from '../api/resources/lists.ts'
12
+ import { Chip } from '../components/Chip.tsx'
13
+ import { ColumnPicker } from '../components/ColumnPicker.tsx'
14
+ import { DataTable } from '../components/DataTable.tsx'
15
+ import type { Column } from '../components/DataTable.tsx'
16
+ import { FilterBar, PageHeader } from '../components/PageHeader.tsx'
17
+ import { Paginator } from '../components/Paginator.tsx'
18
+ import { ErrorPanel, LoadingPanel } from '../components/QueryState.tsx'
19
+ import { formatDate } from '../lib/dates.ts'
20
+ import { useListView } from '../lib/listView.ts'
21
+ import { serverSortOnly } from '../lib/sort.ts'
22
+
23
+ const DEFAULT_VISIBLE_KEYS: readonly string[] = ['name', 'type', 'members', 'description']
24
+
25
+ const SERVER_SORT_KEYS: readonly string[] = ['name', 'created_at', 'updated_at']
26
+
27
+ /**
28
+ * The Lists index.
29
+ *
30
+ * A list holds records of one type, chosen at creation and never changed. The
31
+ * "Add list" button opens a small inline form that captures name and type; on
32
+ * create, the user is taken straight to the list's detail page.
33
+ */
34
+ export function ListsPage(): React.JSX.Element {
35
+ const [term, setTerm] = useState('')
36
+ const [creating, setCreating] = useState(false)
37
+ const [sort, setSort] = useState<string | undefined>(undefined)
38
+ const navigate = useNavigate()
39
+ const hasFilter = term.trim().length > 0
40
+ const lists = useLists({
41
+ term: hasFilter ? term.trim() : undefined,
42
+ sort: serverSortOnly(sort, SERVER_SORT_KEYS),
43
+ })
44
+ const createList = useCreateList()
45
+ const timezone = useTimezone()
46
+
47
+ const columns: readonly Column<List>[] = [
48
+ {
49
+ key: 'name',
50
+ header: 'Name',
51
+ sortKey: 'name',
52
+ render: (list) => <span className="font-medium text-ink">{list.name}</span>,
53
+ },
54
+ {
55
+ key: 'type',
56
+ header: 'Type',
57
+ getSortValue: (list) => RECORD_TARGET_TYPE_LABELS[list.targetType],
58
+ render: (list) => <Chip>{RECORD_TARGET_TYPE_LABELS[list.targetType]}</Chip>,
59
+ },
60
+ {
61
+ key: 'members',
62
+ header: 'Members',
63
+ getSortValue: (list) => list.memberCount,
64
+ render: (list) => String(list.memberCount),
65
+ },
66
+ {
67
+ key: 'description',
68
+ header: 'Description',
69
+ getSortValue: (list) => list.description,
70
+ render: (list) => list.description ?? '—',
71
+ },
72
+ {
73
+ key: 'createdAt',
74
+ header: 'Created',
75
+ sortKey: 'created_at',
76
+ render: (list) => formatDate(list.createdAt, timezone),
77
+ },
78
+ {
79
+ key: 'updatedAt',
80
+ header: 'Updated',
81
+ sortKey: 'updated_at',
82
+ render: (list) => formatDate(list.updatedAt, timezone),
83
+ },
84
+ ]
85
+
86
+ const supportedKeys = columns.map((column) => column.key)
87
+ const listView = useListView('lists', supportedKeys, DEFAULT_VISIBLE_KEYS)
88
+ const pickerOptions = columns.map((column) => ({ key: column.key, label: column.header }))
89
+
90
+ return (
91
+ <div className="animate-fade-in">
92
+ <PageHeader
93
+ title="Lists"
94
+ onAdd={() => {
95
+ setCreating((current) => !current)
96
+ }}
97
+ addLabel="Add list"
98
+ actions={
99
+ <ColumnPicker
100
+ options={pickerOptions}
101
+ visibleKeys={listView.visibleKeys}
102
+ onChange={listView.setVisibleKeys}
103
+ />
104
+ }
105
+ />
106
+
107
+ {creating && (
108
+ <CreateListForm
109
+ isPending={createList.isPending}
110
+ error={createList.error}
111
+ onCancel={() => {
112
+ setCreating(false)
113
+ }}
114
+ onSubmit={(input) => {
115
+ createList
116
+ .runAsync(input)
117
+ .then((list) => {
118
+ setCreating(false)
119
+ return navigate(`/lists/${list.id}`)
120
+ })
121
+ .catch(() => undefined)
122
+ }}
123
+ />
124
+ )}
125
+
126
+ <FilterBar value={term} onChange={setTerm} placeholder="Filter by name…" />
127
+
128
+ {lists.error !== null ? (
129
+ <ErrorPanel error={lists.error} />
130
+ ) : lists.isLoading ? (
131
+ <LoadingPanel label="Loading lists…" />
132
+ ) : (
133
+ <>
134
+ <Paginator list={lists} placement="top" />
135
+ <DataTable
136
+ columns={columns}
137
+ rows={lists.records}
138
+ getRowId={(list) => list.id}
139
+ onRowClick={(list) => {
140
+ void navigate(`/lists/${list.id}`)
141
+ }}
142
+ emptyMessage={hasFilter ? 'No lists match this filter' : 'No lists yet'}
143
+ emptyDescription={
144
+ hasFilter ? 'Try a different search term.' : 'Add a list to group records together.'
145
+ }
146
+ emptyAction={
147
+ hasFilter
148
+ ? undefined
149
+ : {
150
+ label: 'Add list',
151
+ onClick: () => {
152
+ setCreating(true)
153
+ },
154
+ }
155
+ }
156
+ sort={sort}
157
+ onSortChange={setSort}
158
+ visibleColumnKeys={listView.visibleKeys}
159
+ />
160
+ <Paginator list={lists} />
161
+ </>
162
+ )}
163
+ </div>
164
+ )
165
+ }
166
+
167
+ interface CreateListFormProps {
168
+ readonly isPending: boolean
169
+ readonly error: Error | null
170
+ readonly onCancel: () => void
171
+ readonly onSubmit: (input: {
172
+ readonly name: string
173
+ readonly targetType: RecordTargetType
174
+ readonly description: string | null
175
+ }) => void
176
+ }
177
+
178
+ function CreateListForm({
179
+ isPending,
180
+ error,
181
+ onCancel,
182
+ onSubmit,
183
+ }: CreateListFormProps): React.JSX.Element {
184
+ const [name, setName] = useState('')
185
+ const [targetType, setTargetType] = useState<RecordTargetType>('person')
186
+ const [description, setDescription] = useState('')
187
+
188
+ function submit(event: FormEvent<HTMLFormElement>): void {
189
+ event.preventDefault()
190
+ const trimmed = name.trim()
191
+
192
+ if (trimmed.length === 0) {
193
+ return
194
+ }
195
+
196
+ onSubmit({
197
+ name: trimmed,
198
+ targetType,
199
+ description: description.trim().length > 0 ? description.trim() : null,
200
+ })
201
+ }
202
+
203
+ return (
204
+ <form
205
+ onSubmit={submit}
206
+ className="mb-4 rounded-md border border-border bg-surface-raised p-4"
207
+ >
208
+ <div className="grid gap-3 sm:grid-cols-2">
209
+ <label className="flex flex-col gap-1">
210
+ <span className="text-[11px] font-semibold tracking-wide text-ink-faint uppercase">
211
+ Name
212
+ </span>
213
+ <input
214
+ value={name}
215
+ onChange={(event) => {
216
+ setName(event.target.value)
217
+ }}
218
+ required
219
+ autoFocus
220
+ placeholder="Priority people"
221
+ className="rounded-md border border-border bg-surface-raised px-2.5 py-1.5 text-[13px] outline-none focus:border-accent focus:ring-2 focus:ring-accent/20"
222
+ />
223
+ </label>
224
+ <label className="flex flex-col gap-1">
225
+ <span className="text-[11px] font-semibold tracking-wide text-ink-faint uppercase">
226
+ Type
227
+ </span>
228
+ <select
229
+ value={targetType}
230
+ onChange={(event) => {
231
+ setTargetType(event.target.value as RecordTargetType)
232
+ }}
233
+ className="rounded-md border border-border bg-surface-raised px-2.5 py-1.5 text-[13px] outline-none focus:border-accent focus:ring-2 focus:ring-accent/20"
234
+ >
235
+ {RECORD_TARGET_TYPES.map((type) => (
236
+ <option key={type} value={type}>
237
+ {RECORD_TARGET_TYPE_LABELS[type]}
238
+ </option>
239
+ ))}
240
+ </select>
241
+ </label>
242
+ <label className="flex flex-col gap-1 sm:col-span-2">
243
+ <span className="text-[11px] font-semibold tracking-wide text-ink-faint uppercase">
244
+ Description
245
+ </span>
246
+ <input
247
+ value={description}
248
+ onChange={(event) => {
249
+ setDescription(event.target.value)
250
+ }}
251
+ placeholder="Optional"
252
+ className="rounded-md border border-border bg-surface-raised px-2.5 py-1.5 text-[13px] outline-none focus:border-accent focus:ring-2 focus:ring-accent/20"
253
+ />
254
+ </label>
255
+ </div>
256
+
257
+ {error !== null && (
258
+ <div className="mt-3">
259
+ <ErrorPanel error={error} />
260
+ </div>
261
+ )}
262
+
263
+ <div className="mt-3 flex gap-2">
264
+ <button
265
+ type="submit"
266
+ disabled={isPending || name.trim().length === 0}
267
+ className="rounded-md bg-accent px-3 py-1.5 text-[12px] font-medium text-accent-ink transition hover:bg-accent-strong disabled:opacity-50"
268
+ >
269
+ {isPending ? 'Creating…' : 'Create list'}
270
+ </button>
271
+ <button
272
+ type="button"
273
+ onClick={onCancel}
274
+ className="rounded-md px-2.5 py-1.5 text-[12px] font-medium text-ink-muted hover:text-ink"
275
+ >
276
+ Cancel
277
+ </button>
278
+ </div>
279
+ </form>
280
+ )
281
+ }
@@ -1,5 +1,4 @@
1
1
  import type { Opportunity, PipelineStage } 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'
@@ -11,16 +10,21 @@ import {
11
10
  } from '../api/resources/opportunities.ts'
12
11
  import { usePipelineStages } from '../api/resources/pipelineStages.ts'
13
12
  import { MAX_PAGE_SIZE, usePlanItems } from '../api/resources/planItems.ts'
13
+ import { useTimezone } from '../api/resources/account.ts'
14
14
  import { Chip } from '../components/Chip.tsx'
15
15
  import type { ChipTone } from '../components/Chip.tsx'
16
+ import { ColumnPicker } from '../components/ColumnPicker.tsx'
16
17
  import { DataTable } from '../components/DataTable.tsx'
17
18
  import type { Column, DataTableGroup } from '../components/DataTable.tsx'
18
19
  import { KanbanBoard } from '../components/KanbanBoard.tsx'
19
20
  import { PageHeader } from '../components/PageHeader.tsx'
21
+ import { Paginator } from '../components/Paginator.tsx'
20
22
  import { ErrorPanel, LoadingPanel } from '../components/QueryState.tsx'
21
23
  import { SegmentedControl } from '../components/SegmentedControl.tsx'
22
- import { formatDay } from '../lib/dates.ts'
24
+ import { formatDate, formatDay } from '../lib/dates.ts'
25
+ import { useListView } from '../lib/listView.ts'
23
26
  import { DUE_BUCKETS, byDateThenTitle, dueBucketFor, nextOpenByTarget } from '../lib/plan.ts'
27
+ import { serverSortOnly } from '../lib/sort.ts'
24
28
 
25
29
  /**
26
30
  * The Opportunities pipeline: grants, accelerators, tenders, press, speaking.
@@ -32,6 +36,31 @@ type BoardView = 'list' | 'columns'
32
36
  type PipelineScope = 'open' | 'all'
33
37
  type ListGrouping = 'stage' | 'due'
34
38
 
39
+ const SCOPES: readonly PipelineScope[] = ['open', 'all']
40
+ const GROUPINGS: readonly ListGrouping[] = ['stage', 'due']
41
+
42
+ function readScope(stored: string | undefined): PipelineScope {
43
+ return SCOPES.includes(stored as PipelineScope) ? (stored as PipelineScope) : 'open'
44
+ }
45
+
46
+ function readGrouping(stored: string | undefined): ListGrouping {
47
+ return GROUPINGS.includes(stored as ListGrouping) ? (stored as ListGrouping) : 'stage'
48
+ }
49
+
50
+ const COLUMN_KEYS: readonly string[] = [
51
+ 'name',
52
+ 'kind',
53
+ 'company',
54
+ 'stage',
55
+ 'nextPlan',
56
+ 'owner',
57
+ 'close',
58
+ 'tags',
59
+ 'summary',
60
+ 'createdAt',
61
+ 'updatedAt',
62
+ ]
63
+
35
64
  /** Tones keyed by the seeded slugs. A renamed stage keeps its slug and its tone. */
36
65
  const STAGE_TONES: Readonly<Record<string, ChipTone>> = {
37
66
  won: 'success',
@@ -44,19 +73,33 @@ function stageTone(stage: PipelineStage): ChipTone {
44
73
  return STAGE_TONES[stage.slug] ?? 'neutral'
45
74
  }
46
75
 
76
+ const DEFAULT_VISIBLE_KEYS: readonly string[] = [
77
+ 'name',
78
+ 'kind',
79
+ 'company',
80
+ 'nextPlan',
81
+ 'owner',
82
+ 'close',
83
+ ]
84
+
85
+ const SERVER_SORT_KEYS: readonly string[] = ['name', 'created_at', 'updated_at']
86
+
47
87
  export function OpportunitiesPage(): React.JSX.Element {
48
88
  const navigate = useNavigate()
49
- const [view, setView] = useState<BoardView>('list')
50
- const [scope, setScope] = useState<PipelineScope>('open')
51
- const [grouping, setGrouping] = useState<ListGrouping>('stage')
52
- const [sort, setSort] = useState<string | undefined>(undefined)
89
+
90
+ const listView = useListView('opportunities', COLUMN_KEYS, DEFAULT_VISIBLE_KEYS)
91
+ const view: BoardView = listView.mode ?? 'columns'
92
+ const scope = readScope(listView.scope)
93
+ const grouping = readGrouping(listView.grouping)
94
+ const sort = listView.sort
53
95
 
54
96
  const stages = usePipelineStages('opportunity')
55
- const opportunities = useOpportunities({ sort })
97
+ const opportunities = useOpportunities({ sort: serverSortOnly(sort, SERVER_SORT_KEYS) })
56
98
  const companies = useCompanies({ limit: 200 })
57
99
  const members = useMembers()
58
100
  const createOpportunity = useCreateOpportunity()
59
101
  const updateOpportunity = useUpdateOpportunity()
102
+ const timezone = useTimezone()
60
103
 
61
104
  const allStages = [...stages.records].sort((a, b) => a.sortOrder - b.sortOrder)
62
105
  const visibleStages = scope === 'open' ? allStages.filter((stage) => stage.open) : allStages
@@ -79,7 +122,7 @@ export function OpportunitiesPage(): React.JSX.Element {
79
122
  { enabled: askedIds.length > 0 },
80
123
  )
81
124
  const nextPlanByOpportunity = nextOpenByTarget(planItems.records)
82
- const plansTruncated = visibleOpportunities.length > MAX_PAGE_SIZE || planItems.hasMore
125
+ const plansTruncated = visibleOpportunities.length > MAX_PAGE_SIZE || planItems.hasNext
83
126
 
84
127
  const companyNameById = new Map(companies.records.map((company) => [company.id, company.name]))
85
128
 
@@ -89,6 +132,8 @@ export function OpportunitiesPage(): React.JSX.Element {
89
132
  await navigate(`/opportunities/${opportunity.id}`)
90
133
  }
91
134
 
135
+ const stageLabelById = new Map(allStages.map((stage) => [stage.id, stage.label]))
136
+
92
137
  const columns: readonly Column<Opportunity>[] = [
93
138
  {
94
139
  key: 'name',
@@ -99,19 +144,29 @@ export function OpportunitiesPage(): React.JSX.Element {
99
144
  {
100
145
  key: 'kind',
101
146
  header: 'Kind',
147
+ getSortValue: (opportunity) => opportunity.kind || null,
102
148
  render: (opportunity) => (opportunity.kind.length > 0 ? opportunity.kind : '—'),
103
149
  },
104
150
  {
105
151
  key: 'company',
106
152
  header: 'Company',
153
+ getSortValue: (opportunity) =>
154
+ opportunity.companyId === null ? null : (companyNameById.get(opportunity.companyId) ?? null),
107
155
  render: (opportunity) =>
108
156
  opportunity.companyId === null
109
157
  ? '—'
110
158
  : (companyNameById.get(opportunity.companyId) ?? '—'),
111
159
  },
160
+ {
161
+ key: 'stage',
162
+ header: 'Stage',
163
+ getSortValue: (opportunity) => stageLabelById.get(opportunity.stageId) ?? null,
164
+ render: (opportunity) => stageLabelById.get(opportunity.stageId) ?? '—',
165
+ },
112
166
  {
113
167
  key: 'nextPlan',
114
168
  header: 'Next plan',
169
+ getSortValue: (opportunity) => nextPlanByOpportunity.get(opportunity.id)?.date ?? null,
115
170
  render: (opportunity) => {
116
171
  const next = nextPlanByOpportunity.get(opportunity.id)
117
172
 
@@ -132,6 +187,10 @@ export function OpportunitiesPage(): React.JSX.Element {
132
187
  {
133
188
  key: 'owner',
134
189
  header: 'Owner',
190
+ getSortValue: (opportunity) =>
191
+ opportunity.ownerId === null
192
+ ? null
193
+ : (members.nameById.get(opportunity.ownerId) ?? 'Unknown'),
135
194
  render: (opportunity) =>
136
195
  opportunity.ownerId === null
137
196
  ? '—'
@@ -141,14 +200,53 @@ export function OpportunitiesPage(): React.JSX.Element {
141
200
  key: 'close',
142
201
  header: 'Close',
143
202
  className: 'w-28',
203
+ getSortValue: (opportunity) => opportunity.expectedClose,
144
204
  render: (opportunity) => (
145
205
  <span className="font-mono text-[12px] text-ink-muted">
146
206
  {opportunity.expectedClose === null ? '—' : formatDay(opportunity.expectedClose)}
147
207
  </span>
148
208
  ),
149
209
  },
210
+ {
211
+ key: 'tags',
212
+ header: 'Tags',
213
+ getSortValue: (opportunity) => opportunity.tags.join(', ') || null,
214
+ render: (opportunity) =>
215
+ opportunity.tags.length === 0 ? '—' : (
216
+ <span className="flex flex-wrap gap-1">
217
+ {opportunity.tags.map((tag) => (
218
+ <Chip key={tag}>
219
+ <span className="text-[10px]">{tag}</span>
220
+ </Chip>
221
+ ))}
222
+ </span>
223
+ ),
224
+ },
225
+ {
226
+ key: 'summary',
227
+ header: 'Summary',
228
+ getSortValue: (opportunity) => opportunity.summary || null,
229
+ render: (opportunity) =>
230
+ opportunity.summary.length === 0 ? '—' : (
231
+ <span className="text-ink-muted">{opportunity.summary}</span>
232
+ ),
233
+ },
234
+ {
235
+ key: 'createdAt',
236
+ header: 'Created',
237
+ sortKey: 'created_at',
238
+ render: (opportunity) => formatDate(opportunity.createdAt, timezone),
239
+ },
240
+ {
241
+ key: 'updatedAt',
242
+ header: 'Updated',
243
+ sortKey: 'updated_at',
244
+ render: (opportunity) => formatDate(opportunity.updatedAt, timezone),
245
+ },
150
246
  ]
151
247
 
248
+ const pickerOptions = columns.map((column) => ({ key: column.key, label: column.header }))
249
+
152
250
  const stageGroups: readonly DataTableGroup<Opportunity>[] = visibleStages.map((stage) => ({
153
251
  id: stage.id,
154
252
  label: <Chip tone={stageTone(stage)}>{stage.label}</Chip>,
@@ -219,7 +317,9 @@ export function OpportunitiesPage(): React.JSX.Element {
219
317
  <SegmentedControl
220
318
  ariaLabel="Stage scope"
221
319
  value={scope}
222
- onChange={setScope}
320
+ onChange={(next) => {
321
+ listView.setScope(next)
322
+ }}
223
323
  options={[
224
324
  { id: 'open', label: 'Open' },
225
325
  { id: 'all', label: 'All' },
@@ -229,7 +329,9 @@ export function OpportunitiesPage(): React.JSX.Element {
229
329
  <SegmentedControl
230
330
  ariaLabel="Group the list"
231
331
  value={grouping}
232
- onChange={setGrouping}
332
+ onChange={(next) => {
333
+ listView.setGrouping(next)
334
+ }}
233
335
  options={[
234
336
  { id: 'stage', label: 'By stage' },
235
337
  { id: 'due', label: 'By due' },
@@ -239,12 +341,21 @@ export function OpportunitiesPage(): React.JSX.Element {
239
341
  <SegmentedControl
240
342
  ariaLabel="Board or list"
241
343
  value={view}
242
- onChange={setView}
344
+ onChange={(next) => {
345
+ listView.setMode(next)
346
+ }}
243
347
  options={[
244
348
  { id: 'list', label: 'List' },
245
349
  { id: 'columns', label: 'Board' },
246
350
  ]}
247
351
  />
352
+ {view === 'list' && (
353
+ <ColumnPicker
354
+ options={pickerOptions}
355
+ visibleKeys={listView.visibleKeys}
356
+ onChange={listView.setVisibleKeys}
357
+ />
358
+ )}
248
359
  </>
249
360
  }
250
361
  />
@@ -261,6 +372,7 @@ export function OpportunitiesPage(): React.JSX.Element {
261
372
  <LoadingPanel label="Loading opportunities…" />
262
373
  ) : (
263
374
  <>
375
+ {view === 'list' && <Paginator list={opportunities} placement="top" />}
264
376
  {view === 'columns' ? (
265
377
  <KanbanBoard
266
378
  stages={visibleStages.map((stage) => ({ id: stage.id, label: stage.label }))}
@@ -307,13 +419,16 @@ export function OpportunitiesPage(): React.JSX.Element {
307
419
  ? {
308
420
  label: 'Show all opportunities',
309
421
  onClick: () => {
310
- setScope('all')
422
+ listView.setScope('all')
311
423
  },
312
424
  }
313
425
  : undefined
314
426
  }
315
427
  sort={sort}
316
- onSortChange={setSort}
428
+ onSortChange={(next) => {
429
+ listView.setSort(next)
430
+ }}
431
+ visibleColumnKeys={listView.visibleKeys}
317
432
  />
318
433
  )}
319
434
  {updateOpportunity.error !== null && (
@@ -321,17 +436,8 @@ export function OpportunitiesPage(): React.JSX.Element {
321
436
  <ErrorPanel error={updateOpportunity.error} />
322
437
  </div>
323
438
  )}
324
- {opportunities.hasMore && (
325
- <button
326
- type="button"
327
- onClick={opportunities.loadMore}
328
- disabled={opportunities.isLoadingMore}
329
- 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"
330
- >
331
- {opportunities.isLoadingMore ? 'Loading…' : 'Load more'}
332
- </button>
333
- )}
334
- {companies.hasMore && (
439
+ {view === 'list' && <Paginator list={opportunities} />}
440
+ {companies.hasNext && (
335
441
  <p className="mt-2 text-[11px] text-ink-faint">
336
442
  More companies exist than one page returns, so some company names may show as “—”.
337
443
  </p>