@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.
- package/dist/api/resource.d.ts +59 -6
- package/dist/api/resource.d.ts.map +1 -1
- package/dist/api/resource.js +106 -24
- package/dist/api/resource.js.map +1 -1
- package/dist/api/resources/companies.d.ts +2 -0
- package/dist/api/resources/companies.d.ts.map +1 -1
- package/dist/api/resources/companies.js +5 -1
- package/dist/api/resources/companies.js.map +1 -1
- package/dist/api/resources/consentPurposes.d.ts +13 -0
- package/dist/api/resources/consentPurposes.d.ts.map +1 -0
- package/dist/api/resources/consentPurposes.js +39 -0
- package/dist/api/resources/consentPurposes.js.map +1 -0
- package/dist/api/resources/customFields.d.ts +15 -0
- package/dist/api/resources/customFields.d.ts.map +1 -0
- package/dist/api/resources/customFields.js +41 -0
- package/dist/api/resources/customFields.js.map +1 -0
- package/dist/api/resources/decisions.d.ts +2 -0
- package/dist/api/resources/decisions.d.ts.map +1 -1
- package/dist/api/resources/decisions.js +1 -0
- package/dist/api/resources/decisions.js.map +1 -1
- package/dist/api/resources/emailDomainLinker.d.ts +16 -0
- package/dist/api/resources/emailDomainLinker.d.ts.map +1 -0
- package/dist/api/resources/emailDomainLinker.js +16 -0
- package/dist/api/resources/emailDomainLinker.js.map +1 -0
- package/dist/api/resources/enquiries.d.ts +39 -0
- package/dist/api/resources/enquiries.d.ts.map +1 -0
- package/dist/api/resources/enquiries.js +68 -0
- package/dist/api/resources/enquiries.js.map +1 -0
- package/dist/api/resources/forms.d.ts +21 -1
- package/dist/api/resources/forms.d.ts.map +1 -1
- package/dist/api/resources/forms.js +44 -17
- package/dist/api/resources/forms.js.map +1 -1
- package/dist/api/resources/handbookPages.d.ts +1 -1
- package/dist/api/resources/handbookPages.js +1 -1
- package/dist/api/resources/importJobs.d.ts +5 -0
- package/dist/api/resources/importJobs.d.ts.map +1 -1
- package/dist/api/resources/importJobs.js +3 -0
- package/dist/api/resources/importJobs.js.map +1 -1
- package/dist/api/resources/listMembers.d.ts +27 -0
- package/dist/api/resources/listMembers.d.ts.map +1 -0
- package/dist/api/resources/listMembers.js +108 -0
- package/dist/api/resources/listMembers.js.map +1 -0
- package/dist/api/resources/lists.d.ts +14 -0
- package/dist/api/resources/lists.d.ts.map +1 -0
- package/dist/api/resources/lists.js +34 -0
- package/dist/api/resources/lists.js.map +1 -0
- package/dist/api/resources/opportunities.d.ts +2 -0
- package/dist/api/resources/opportunities.d.ts.map +1 -1
- package/dist/api/resources/opportunities.js +1 -0
- package/dist/api/resources/opportunities.js.map +1 -1
- package/dist/api/resources/people.d.ts.map +1 -1
- package/dist/api/resources/people.js +4 -1
- package/dist/api/resources/people.js.map +1 -1
- package/dist/api/resources/roles.d.ts +2 -0
- package/dist/api/resources/roles.d.ts.map +1 -1
- package/dist/api/resources/roles.js +1 -1
- package/dist/api/resources/roles.js.map +1 -1
- package/dist/api/resources/sampleData.d.ts +17 -0
- package/dist/api/resources/sampleData.d.ts.map +1 -0
- package/dist/api/resources/sampleData.js +31 -0
- package/dist/api/resources/sampleData.js.map +1 -0
- package/dist/api/resources/webhooks.d.ts.map +1 -1
- package/dist/api/resources/webhooks.js +7 -22
- package/dist/api/resources/webhooks.js.map +1 -1
- package/dist/app/KelpieApp.d.ts.map +1 -1
- package/dist/app/KelpieApp.js +9 -1
- package/dist/app/KelpieApp.js.map +1 -1
- package/dist/components/ActivitiesPanel.d.ts.map +1 -1
- package/dist/components/ActivitiesPanel.js +3 -1
- package/dist/components/ActivitiesPanel.js.map +1 -1
- package/dist/components/ColumnPicker.d.ts +24 -0
- package/dist/components/ColumnPicker.d.ts.map +1 -0
- package/dist/components/ColumnPicker.js +56 -0
- package/dist/components/ColumnPicker.js.map +1 -0
- package/dist/components/CustomFieldsPanel.d.ts +23 -0
- package/dist/components/CustomFieldsPanel.d.ts.map +1 -0
- package/dist/components/CustomFieldsPanel.js +116 -0
- package/dist/components/CustomFieldsPanel.js.map +1 -0
- package/dist/components/DataTable.d.ts +16 -1
- package/dist/components/DataTable.d.ts.map +1 -1
- package/dist/components/DataTable.js +51 -10
- package/dist/components/DataTable.js.map +1 -1
- package/dist/components/DecisionsPanel.d.ts.map +1 -1
- package/dist/components/DecisionsPanel.js +4 -3
- package/dist/components/DecisionsPanel.js.map +1 -1
- package/dist/components/EntitySearch.d.ts.map +1 -1
- package/dist/components/EntitySearch.js +11 -5
- package/dist/components/EntitySearch.js.map +1 -1
- package/dist/components/FormsPanel.d.ts +18 -0
- package/dist/components/FormsPanel.d.ts.map +1 -0
- package/dist/components/FormsPanel.js +37 -0
- package/dist/components/FormsPanel.js.map +1 -0
- package/dist/components/ListsPanel.d.ts +14 -0
- package/dist/components/ListsPanel.d.ts.map +1 -0
- package/dist/components/ListsPanel.js +56 -0
- package/dist/components/ListsPanel.js.map +1 -0
- package/dist/components/NotesPanel.d.ts.map +1 -1
- package/dist/components/NotesPanel.js +6 -5
- package/dist/components/NotesPanel.js.map +1 -1
- package/dist/components/Paginator.d.ts +26 -0
- package/dist/components/Paginator.d.ts.map +1 -0
- package/dist/components/Paginator.js +16 -0
- package/dist/components/Paginator.js.map +1 -0
- package/dist/components/PlanAttention.d.ts +3 -2
- package/dist/components/PlanAttention.d.ts.map +1 -1
- package/dist/components/PlanAttention.js +7 -3
- package/dist/components/PlanAttention.js.map +1 -1
- package/dist/components/PlanPanel.d.ts.map +1 -1
- package/dist/components/PlanPanel.js +2 -1
- package/dist/components/PlanPanel.js.map +1 -1
- package/dist/components/Shell.d.ts.map +1 -1
- package/dist/components/Shell.js +4 -0
- package/dist/components/Shell.js.map +1 -1
- package/dist/components/useHasCustomFields.d.ts +13 -0
- package/dist/components/useHasCustomFields.d.ts.map +1 -0
- package/dist/components/useHasCustomFields.js +16 -0
- package/dist/components/useHasCustomFields.js.map +1 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/listView.d.ts +15 -0
- package/dist/lib/listView.d.ts.map +1 -0
- package/dist/lib/listView.js +66 -0
- package/dist/lib/listView.js.map +1 -0
- package/dist/lib/sort.d.ts +43 -0
- package/dist/lib/sort.d.ts.map +1 -0
- package/dist/lib/sort.js +102 -0
- package/dist/lib/sort.js.map +1 -0
- package/dist/pages/CompaniesPage.d.ts.map +1 -1
- package/dist/pages/CompaniesPage.js +88 -3
- package/dist/pages/CompaniesPage.js.map +1 -1
- package/dist/pages/CompanyDetail.d.ts.map +1 -1
- package/dist/pages/CompanyDetail.js +40 -15
- package/dist/pages/CompanyDetail.js.map +1 -1
- package/dist/pages/DealDetail.d.ts.map +1 -1
- package/dist/pages/DealDetail.js +20 -1
- package/dist/pages/DealDetail.js.map +1 -1
- package/dist/pages/DealsPage.d.ts.map +1 -1
- package/dist/pages/DealsPage.js +134 -14
- package/dist/pages/DealsPage.js.map +1 -1
- package/dist/pages/DecisionsPage.d.ts.map +1 -1
- package/dist/pages/DecisionsPage.js +52 -8
- package/dist/pages/DecisionsPage.js.map +1 -1
- package/dist/pages/EnquiriesPage.d.ts +2 -0
- package/dist/pages/EnquiriesPage.d.ts.map +1 -0
- package/dist/pages/EnquiriesPage.js +251 -0
- package/dist/pages/EnquiriesPage.js.map +1 -0
- package/dist/pages/EnquiryDetail.d.ts +2 -0
- package/dist/pages/EnquiryDetail.d.ts.map +1 -0
- package/dist/pages/EnquiryDetail.js +207 -0
- package/dist/pages/EnquiryDetail.js.map +1 -0
- package/dist/pages/EnquiryStageSettingsPage.d.ts +3 -0
- package/dist/pages/EnquiryStageSettingsPage.d.ts.map +1 -0
- package/dist/pages/EnquiryStageSettingsPage.js +34 -0
- package/dist/pages/EnquiryStageSettingsPage.js.map +1 -0
- package/dist/pages/FormDetail.d.ts.map +1 -1
- package/dist/pages/FormDetail.js +19 -1
- package/dist/pages/FormDetail.js.map +1 -1
- package/dist/pages/FormsPage.d.ts.map +1 -1
- package/dist/pages/FormsPage.js +53 -2
- package/dist/pages/FormsPage.js.map +1 -1
- package/dist/pages/FundraisingPage.d.ts.map +1 -1
- package/dist/pages/FundraisingPage.js +116 -14
- package/dist/pages/FundraisingPage.js.map +1 -1
- package/dist/pages/HiringPage.d.ts.map +1 -1
- package/dist/pages/HiringPage.js +27 -4
- package/dist/pages/HiringPage.js.map +1 -1
- package/dist/pages/ListDetail.d.ts +11 -0
- package/dist/pages/ListDetail.d.ts.map +1 -0
- package/dist/pages/ListDetail.js +214 -0
- package/dist/pages/ListDetail.js.map +1 -0
- package/dist/pages/ListsPage.d.ts +9 -0
- package/dist/pages/ListsPage.d.ts.map +1 -0
- package/dist/pages/ListsPage.js +125 -0
- package/dist/pages/ListsPage.js.map +1 -0
- package/dist/pages/OpportunitiesPage.d.ts.map +1 -1
- package/dist/pages/OpportunitiesPage.js +97 -14
- package/dist/pages/OpportunitiesPage.js.map +1 -1
- package/dist/pages/OpportunityDetail.d.ts.map +1 -1
- package/dist/pages/OpportunityDetail.js +64 -3
- package/dist/pages/OpportunityDetail.js.map +1 -1
- package/dist/pages/PartnershipDetail.d.ts.map +1 -1
- package/dist/pages/PartnershipDetail.js +19 -1
- package/dist/pages/PartnershipDetail.js.map +1 -1
- package/dist/pages/PartnershipsPage.d.ts.map +1 -1
- package/dist/pages/PartnershipsPage.js +111 -14
- package/dist/pages/PartnershipsPage.js.map +1 -1
- package/dist/pages/PeoplePage.d.ts.map +1 -1
- package/dist/pages/PeoplePage.js +119 -3
- package/dist/pages/PeoplePage.js.map +1 -1
- package/dist/pages/PersonDetail.d.ts.map +1 -1
- package/dist/pages/PersonDetail.js +67 -5
- package/dist/pages/PersonDetail.js.map +1 -1
- package/dist/pages/PlanningPage.d.ts.map +1 -1
- package/dist/pages/PlanningPage.js +88 -17
- package/dist/pages/PlanningPage.js.map +1 -1
- package/dist/pages/RaiseDetail.d.ts.map +1 -1
- package/dist/pages/RaiseDetail.js +13 -1
- package/dist/pages/RaiseDetail.js.map +1 -1
- package/dist/pages/RoleDetail.d.ts.map +1 -1
- package/dist/pages/RoleDetail.js +5 -4
- package/dist/pages/RoleDetail.js.map +1 -1
- package/dist/pages/SearchPage.d.ts.map +1 -1
- package/dist/pages/SearchPage.js +1 -0
- package/dist/pages/SearchPage.js.map +1 -1
- package/dist/pages/admin/DataPage.d.ts.map +1 -1
- package/dist/pages/admin/DataPage.js +126 -13
- package/dist/pages/admin/DataPage.js.map +1 -1
- package/dist/pages/admin/FieldsPage.d.ts +2 -0
- package/dist/pages/admin/FieldsPage.d.ts.map +1 -0
- package/dist/pages/admin/FieldsPage.js +164 -0
- package/dist/pages/admin/FieldsPage.js.map +1 -0
- package/dist/pages/admin/PrivacyPage.d.ts +9 -0
- package/dist/pages/admin/PrivacyPage.d.ts.map +1 -0
- package/dist/pages/admin/PrivacyPage.js +170 -0
- package/dist/pages/admin/PrivacyPage.js.map +1 -0
- package/dist/pages/admin/WebhookDeliveries.d.ts.map +1 -1
- package/dist/pages/admin/WebhookDeliveries.js +3 -2
- package/dist/pages/admin/WebhookDeliveries.js.map +1 -1
- package/dist/pages/admin/WebhooksPage.d.ts.map +1 -1
- package/dist/pages/admin/WebhooksPage.js +3 -2
- package/dist/pages/admin/WebhooksPage.js.map +1 -1
- package/dist/pages/admin/WorkspaceSettingsPage.d.ts.map +1 -1
- package/dist/pages/admin/WorkspaceSettingsPage.js +45 -2
- package/dist/pages/admin/WorkspaceSettingsPage.js.map +1 -1
- package/dist/pages/dashboard/attention.d.ts.map +1 -1
- package/dist/pages/dashboard/attention.js +3 -0
- package/dist/pages/dashboard/attention.js.map +1 -1
- package/dist/pages/forms/AddFieldMenu.d.ts +17 -0
- package/dist/pages/forms/AddFieldMenu.d.ts.map +1 -0
- package/dist/pages/forms/AddFieldMenu.js +43 -0
- package/dist/pages/forms/AddFieldMenu.js.map +1 -0
- package/dist/pages/forms/EmbedPanel.d.ts +4 -0
- package/dist/pages/forms/EmbedPanel.d.ts.map +1 -1
- package/dist/pages/forms/EmbedPanel.js +62 -3
- package/dist/pages/forms/EmbedPanel.js.map +1 -1
- package/dist/pages/forms/FieldsEditor.d.ts.map +1 -1
- package/dist/pages/forms/FieldsEditor.js +179 -36
- package/dist/pages/forms/FieldsEditor.js.map +1 -1
- package/dist/pages/forms/FormActions.d.ts +6 -0
- package/dist/pages/forms/FormActions.d.ts.map +1 -0
- package/dist/pages/forms/FormActions.js +149 -0
- package/dist/pages/forms/FormActions.js.map +1 -0
- package/dist/pages/forms/FormSettings.d.ts.map +1 -1
- package/dist/pages/forms/FormSettings.js +15 -19
- package/dist/pages/forms/FormSettings.js.map +1 -1
- package/dist/pages/forms/SubmissionDetail.d.ts +2 -0
- package/dist/pages/forms/SubmissionDetail.d.ts.map +1 -0
- package/dist/pages/forms/SubmissionDetail.js +76 -0
- package/dist/pages/forms/SubmissionDetail.js.map +1 -0
- package/dist/pages/forms/SubmissionsTable.d.ts.map +1 -1
- package/dist/pages/forms/SubmissionsTable.js +57 -8
- package/dist/pages/forms/SubmissionsTable.js.map +1 -1
- package/dist/pages/forms/fieldList.d.ts +11 -5
- package/dist/pages/forms/fieldList.d.ts.map +1 -1
- package/dist/pages/forms/fieldList.js +66 -6
- package/dist/pages/forms/fieldList.js.map +1 -1
- package/dist/pages/forms/template.d.ts +17 -0
- package/dist/pages/forms/template.d.ts.map +1 -1
- package/dist/pages/forms/template.js +93 -0
- package/dist/pages/forms/template.js.map +1 -1
- package/dist/pages/handbook/HandbookPage.js +1 -1
- package/dist/pages/hiringDirectory.js +2 -2
- package/dist/pages/onboarding/WorkspaceStepPage.d.ts.map +1 -1
- package/dist/pages/onboarding/WorkspaceStepPage.js +15 -2
- package/dist/pages/onboarding/WorkspaceStepPage.js.map +1 -1
- package/dist/pages/positionDirectory.js +2 -2
- package/package.json +2 -2
- package/src/api/resource.ts +155 -32
- package/src/api/resources/companies.ts +7 -1
- package/src/api/resources/consentPurposes.ts +83 -0
- package/src/api/resources/customFields.ts +88 -0
- package/src/api/resources/decisions.ts +3 -0
- package/src/api/resources/emailDomainLinker.ts +43 -0
- package/src/api/resources/enquiries.ts +118 -0
- package/src/api/resources/forms.ts +59 -19
- package/src/api/resources/handbookPages.ts +1 -1
- package/src/api/resources/importJobs.ts +9 -0
- package/src/api/resources/listMembers.ts +173 -0
- package/src/api/resources/lists.ts +61 -0
- package/src/api/resources/opportunities.ts +3 -0
- package/src/api/resources/people.ts +4 -1
- package/src/api/resources/roles.ts +3 -1
- package/src/api/resources/sampleData.ts +57 -0
- package/src/api/resources/webhooks.ts +8 -24
- package/src/app/KelpieApp.tsx +16 -0
- package/src/components/ActivitiesPanel.tsx +3 -10
- package/src/components/ColumnPicker.tsx +124 -0
- package/src/components/CustomFieldsPanel.tsx +330 -0
- package/src/components/DataTable.tsx +89 -10
- package/src/components/DecisionsPanel.tsx +4 -12
- package/src/components/EntitySearch.tsx +12 -5
- package/src/components/FormsPanel.tsx +86 -0
- package/src/components/ListsPanel.tsx +176 -0
- package/src/components/NotesPanel.tsx +6 -14
- package/src/components/Paginator.tsx +78 -0
- package/src/components/PlanAttention.tsx +20 -3
- package/src/components/PlanPanel.tsx +2 -10
- package/src/components/Shell.tsx +4 -0
- package/src/components/useHasCustomFields.ts +18 -0
- package/src/index.ts +10 -1
- package/src/lib/listView.ts +149 -0
- package/src/lib/sort.ts +143 -0
- package/src/pages/CompaniesPage.tsx +125 -11
- package/src/pages/CompanyDetail.tsx +98 -63
- package/src/pages/DealDetail.tsx +34 -0
- package/src/pages/DealsPage.tsx +178 -24
- package/src/pages/DecisionsPage.tsx +75 -18
- package/src/pages/EnquiriesPage.tsx +434 -0
- package/src/pages/EnquiryDetail.tsx +554 -0
- package/src/pages/EnquiryStageSettingsPage.tsx +63 -0
- package/src/pages/FormDetail.tsx +23 -1
- package/src/pages/FormsPage.tsx +81 -17
- package/src/pages/FundraisingPage.tsx +152 -24
- package/src/pages/HiringPage.tsx +39 -11
- package/src/pages/ListDetail.tsx +470 -0
- package/src/pages/ListsPage.tsx +281 -0
- package/src/pages/OpportunitiesPage.tsx +130 -24
- package/src/pages/OpportunityDetail.tsx +168 -2
- package/src/pages/PartnershipDetail.tsx +35 -0
- package/src/pages/PartnershipsPage.tsx +149 -24
- package/src/pages/PeoplePage.tsx +157 -11
- package/src/pages/PersonDetail.tsx +157 -6
- package/src/pages/PlanningPage.tsx +259 -29
- package/src/pages/RaiseDetail.tsx +24 -0
- package/src/pages/RoleDetail.tsx +21 -26
- package/src/pages/SearchPage.tsx +1 -0
- package/src/pages/admin/DataPage.tsx +324 -13
- package/src/pages/admin/FieldsPage.tsx +455 -0
- package/src/pages/admin/PrivacyPage.tsx +461 -0
- package/src/pages/admin/WebhookDeliveries.tsx +10 -15
- package/src/pages/admin/WebhooksPage.tsx +14 -19
- package/src/pages/admin/WorkspaceSettingsPage.tsx +134 -2
- package/src/pages/dashboard/attention.ts +3 -0
- package/src/pages/forms/AddFieldMenu.tsx +121 -0
- package/src/pages/forms/EmbedPanel.tsx +163 -2
- package/src/pages/forms/FieldsEditor.tsx +539 -165
- package/src/pages/forms/FormActions.tsx +552 -0
- package/src/pages/forms/FormSettings.tsx +29 -70
- package/src/pages/forms/SubmissionDetail.tsx +249 -0
- package/src/pages/forms/SubmissionsTable.tsx +112 -26
- package/src/pages/forms/fieldList.ts +68 -5
- package/src/pages/forms/template.ts +103 -0
- package/src/pages/handbook/HandbookPage.tsx +1 -1
- package/src/pages/hiringDirectory.ts +2 -2
- package/src/pages/onboarding/WorkspaceStepPage.tsx +31 -3
- package/src/pages/positionDirectory.ts +2 -2
- 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 {
|
|
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 {
|
|
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
|
|
83
|
-
|
|
81
|
+
const query: QueryParameters = { status: filters.status }
|
|
82
|
+
|
|
83
|
+
return usePagedList<WebhookDelivery>({
|
|
84
84
|
queryKey: ['webhooks', 'deliveries', webhookId, filters.status ?? 'all'],
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
/**
|
package/src/app/KelpieApp.tsx
CHANGED
|
@@ -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
|
|
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
|
+
}
|