@kelpie/ui 0.6.0 → 0.8.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 +58 -5
- 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/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/forms.d.ts.map +1 -1
- package/dist/api/resources/forms.js +6 -20
- 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 +3 -1
- package/dist/api/resources/importJobs.d.ts.map +1 -1
- package/dist/api/resources/importJobs.js +1 -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/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/publicConfig.d.ts +8 -0
- package/dist/api/resources/publicConfig.d.ts.map +1 -0
- package/dist/api/resources/publicConfig.js +22 -0
- package/dist/api/resources/publicConfig.js.map +1 -0
- 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 +30 -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 +5 -2
- package/dist/app/KelpieApp.js.map +1 -1
- package/dist/components/ActivitiesPanel.d.ts.map +1 -1
- package/dist/components/ActivitiesPanel.js +2 -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/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/EnvironmentBanner.d.ts +14 -0
- package/dist/components/EnvironmentBanner.d.ts.map +1 -0
- package/dist/components/EnvironmentBanner.js +23 -0
- package/dist/components/EnvironmentBanner.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/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 +1 -0
- package/dist/components/Shell.js.map +1 -1
- 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 +17 -14
- package/dist/pages/CompanyDetail.js.map +1 -1
- package/dist/pages/DealDetail.d.ts.map +1 -1
- package/dist/pages/DealDetail.js +3 -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 +50 -8
- package/dist/pages/DecisionsPage.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 +188 -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 +3 -1
- package/dist/pages/OpportunityDetail.js.map +1 -1
- package/dist/pages/PartnershipDetail.d.ts.map +1 -1
- package/dist/pages/PartnershipDetail.js +3 -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 +102 -3
- package/dist/pages/PeoplePage.js.map +1 -1
- package/dist/pages/PersonDetail.d.ts.map +1 -1
- package/dist/pages/PersonDetail.js +5 -3
- package/dist/pages/PersonDetail.js.map +1 -1
- package/dist/pages/PlanningPage.d.ts.map +1 -1
- package/dist/pages/PlanningPage.js +85 -17
- package/dist/pages/PlanningPage.js.map +1 -1
- package/dist/pages/RaiseDetail.d.ts.map +1 -1
- package/dist/pages/RaiseDetail.js +3 -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/admin/DataPage.d.ts.map +1 -1
- package/dist/pages/admin/DataPage.js +126 -16
- package/dist/pages/admin/DataPage.js.map +1 -1
- 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/forms/SubmissionsTable.d.ts.map +1 -1
- package/dist/pages/forms/SubmissionsTable.js +2 -1
- package/dist/pages/forms/SubmissionsTable.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 +149 -31
- package/src/api/resources/companies.ts +7 -1
- package/src/api/resources/decisions.ts +3 -0
- package/src/api/resources/emailDomainLinker.ts +43 -0
- package/src/api/resources/forms.ts +5 -23
- package/src/api/resources/handbookPages.ts +1 -1
- package/src/api/resources/importJobs.ts +4 -0
- package/src/api/resources/listMembers.ts +173 -0
- package/src/api/resources/lists.ts +61 -0
- package/src/api/resources/people.ts +4 -1
- package/src/api/resources/publicConfig.ts +33 -0
- package/src/api/resources/roles.ts +3 -1
- package/src/api/resources/sampleData.ts +56 -0
- package/src/api/resources/webhooks.ts +8 -24
- package/src/app/KelpieApp.tsx +11 -0
- package/src/components/ActivitiesPanel.tsx +2 -10
- package/src/components/ColumnPicker.tsx +124 -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/EnvironmentBanner.tsx +30 -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/PlanPanel.tsx +2 -10
- package/src/components/Shell.tsx +1 -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 +62 -62
- package/src/pages/DealDetail.tsx +3 -0
- package/src/pages/DealsPage.tsx +178 -24
- package/src/pages/DecisionsPage.tsx +73 -18
- 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 +408 -0
- package/src/pages/ListsPage.tsx +281 -0
- package/src/pages/OpportunitiesPage.tsx +130 -24
- package/src/pages/OpportunityDetail.tsx +3 -0
- package/src/pages/PartnershipDetail.tsx +3 -0
- package/src/pages/PartnershipsPage.tsx +149 -24
- package/src/pages/PeoplePage.tsx +137 -11
- package/src/pages/PersonDetail.tsx +5 -3
- package/src/pages/PlanningPage.tsx +256 -29
- package/src/pages/RaiseDetail.tsx +3 -0
- package/src/pages/RoleDetail.tsx +21 -26
- package/src/pages/admin/DataPage.tsx +359 -36
- 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/forms/SubmissionsTable.tsx +3 -10
- 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
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addListMemberBody,
|
|
3
|
+
listMemberSchema,
|
|
4
|
+
listMembershipSchema,
|
|
5
|
+
} from '@kelpie/schemas'
|
|
6
|
+
import type {
|
|
7
|
+
AddListMemberInput,
|
|
8
|
+
ListMember,
|
|
9
|
+
ListMembership,
|
|
10
|
+
RecordTargetType,
|
|
11
|
+
} from '@kelpie/schemas'
|
|
12
|
+
import {
|
|
13
|
+
useMutation,
|
|
14
|
+
useQuery,
|
|
15
|
+
useQueryClient,
|
|
16
|
+
} from '@tanstack/react-query'
|
|
17
|
+
import type { InfiniteData } from '@tanstack/react-query'
|
|
18
|
+
|
|
19
|
+
import type { Page } from '../client.ts'
|
|
20
|
+
import { useApiClient } from '../context.ts'
|
|
21
|
+
import { toError } from '../errors.ts'
|
|
22
|
+
import { usePagedList } from '../resource.ts'
|
|
23
|
+
import type { MutationResult, RecordListResult } from '../resource.ts'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* `/v1/lists/:id/members`. Nested per list, so the standard resource hook cannot
|
|
27
|
+
* mint the path. Built by hand with the same read/write cache invalidation
|
|
28
|
+
* pattern the other resources use.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
type MembersData = InfiniteData<Page<ListMember>, string | null>
|
|
32
|
+
|
|
33
|
+
function membersKey(listId: string): readonly unknown[] {
|
|
34
|
+
return ['lists', 'members', listId]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function membershipsKey(targetType: string, targetId: string): readonly unknown[] {
|
|
38
|
+
return ['lists', 'memberships', targetType, targetId]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The list-level entries whose cached counts change when a member is added or removed. */
|
|
42
|
+
function listsInvalidations(): readonly unknown[] {
|
|
43
|
+
return ['lists', 'list']
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ListMembershipsResult {
|
|
47
|
+
readonly memberships: readonly ListMembership[]
|
|
48
|
+
readonly isLoading: boolean
|
|
49
|
+
readonly error: Error | null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* "Which lists is this record on?"
|
|
54
|
+
*
|
|
55
|
+
* A plain `useQuery` rather than the paged read: the endpoint is not paged.
|
|
56
|
+
* The invalidation key overlaps with `useListMembers` so adding or removing on
|
|
57
|
+
* either side refreshes both views.
|
|
58
|
+
*/
|
|
59
|
+
export function useListMembershipsFor(
|
|
60
|
+
targetType: RecordTargetType | undefined,
|
|
61
|
+
targetId: string | undefined,
|
|
62
|
+
): ListMembershipsResult {
|
|
63
|
+
const client = useApiClient()
|
|
64
|
+
const enabled = targetType !== undefined && targetId !== undefined
|
|
65
|
+
const result = useQuery({
|
|
66
|
+
queryKey: membershipsKey(targetType ?? '', targetId ?? ''),
|
|
67
|
+
queryFn: async () => {
|
|
68
|
+
const page = await client.list(
|
|
69
|
+
'/list-memberships',
|
|
70
|
+
listMembershipSchema.parse,
|
|
71
|
+
{ target_type: targetType, target_id: targetId },
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return page.items
|
|
75
|
+
},
|
|
76
|
+
enabled,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
memberships: result.data ?? [],
|
|
81
|
+
isLoading: result.isPending && enabled,
|
|
82
|
+
error: toError(result.error),
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function useListMembers(listId: string | undefined): RecordListResult<ListMember> {
|
|
87
|
+
return usePagedList<ListMember>({
|
|
88
|
+
queryKey: membersKey(listId ?? ''),
|
|
89
|
+
path: `/lists/${listId ?? ''}/members`,
|
|
90
|
+
decode: listMemberSchema.parse,
|
|
91
|
+
query: {},
|
|
92
|
+
enabled: listId !== undefined,
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface AddMemberArguments {
|
|
97
|
+
readonly listId: string
|
|
98
|
+
readonly input: AddListMemberInput
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function useAddListMember(): MutationResult<AddMemberArguments, ListMember> {
|
|
102
|
+
const client = useApiClient()
|
|
103
|
+
const queryClient = useQueryClient()
|
|
104
|
+
const mutation = useMutation({
|
|
105
|
+
mutationFn: ({ listId, input }: AddMemberArguments) =>
|
|
106
|
+
client.post(`/lists/${listId}/members`, addListMemberBody(input), listMemberSchema.parse),
|
|
107
|
+
onSettled: (_record, _error, { listId }) => {
|
|
108
|
+
void queryClient.invalidateQueries({ queryKey: membersKey(listId) })
|
|
109
|
+
void queryClient.invalidateQueries({ queryKey: listsInvalidations() })
|
|
110
|
+
void queryClient.invalidateQueries({ queryKey: ['lists', 'memberships'] })
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
run: (input) => {
|
|
116
|
+
mutation.mutate(input)
|
|
117
|
+
},
|
|
118
|
+
runAsync: (input) => mutation.mutateAsync(input),
|
|
119
|
+
isPending: mutation.isPending,
|
|
120
|
+
error: toError(mutation.error),
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface RemoveMemberArguments {
|
|
125
|
+
readonly listId: string
|
|
126
|
+
readonly id: string
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function useRemoveListMember(): MutationResult<RemoveMemberArguments, void> {
|
|
130
|
+
const client = useApiClient()
|
|
131
|
+
const queryClient = useQueryClient()
|
|
132
|
+
const mutation = useMutation({
|
|
133
|
+
mutationFn: ({ listId, id }: RemoveMemberArguments) =>
|
|
134
|
+
client.delete(`/lists/${listId}/members/${id}`),
|
|
135
|
+
onMutate: async ({ listId, id }) => {
|
|
136
|
+
await queryClient.cancelQueries({ queryKey: membersKey(listId) })
|
|
137
|
+
const previous = queryClient.getQueryData<MembersData>(membersKey(listId))
|
|
138
|
+
|
|
139
|
+
queryClient.setQueryData<MembersData>(membersKey(listId), (data) =>
|
|
140
|
+
data === undefined
|
|
141
|
+
? data
|
|
142
|
+
: {
|
|
143
|
+
...data,
|
|
144
|
+
pages: data.pages.map((page) => ({
|
|
145
|
+
...page,
|
|
146
|
+
items: page.items.filter((item) => item.id !== id),
|
|
147
|
+
})),
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return { previous }
|
|
152
|
+
},
|
|
153
|
+
onError: (_error, { listId }, context) => {
|
|
154
|
+
if (context?.previous !== undefined) {
|
|
155
|
+
queryClient.setQueryData(membersKey(listId), context.previous)
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
onSettled: (_record, _error, { listId }) => {
|
|
159
|
+
void queryClient.invalidateQueries({ queryKey: membersKey(listId) })
|
|
160
|
+
void queryClient.invalidateQueries({ queryKey: listsInvalidations() })
|
|
161
|
+
void queryClient.invalidateQueries({ queryKey: ['lists', 'memberships'] })
|
|
162
|
+
},
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
run: (input) => {
|
|
167
|
+
mutation.mutate(input)
|
|
168
|
+
},
|
|
169
|
+
runAsync: (input) => mutation.mutateAsync(input),
|
|
170
|
+
isPending: mutation.isPending,
|
|
171
|
+
error: toError(mutation.error),
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createListBody, listBody, listSchema } from '@kelpie/schemas'
|
|
2
|
+
import type { CreateListInput, List, ListInput, RecordTargetType } from '@kelpie/schemas'
|
|
3
|
+
|
|
4
|
+
import type { QueryParameters } from '../client.ts'
|
|
5
|
+
import { createResourceHooks } from '../resource.ts'
|
|
6
|
+
import type {
|
|
7
|
+
ListOptions,
|
|
8
|
+
MutationResult,
|
|
9
|
+
RecordListResult,
|
|
10
|
+
RecordResult,
|
|
11
|
+
UpdateArguments,
|
|
12
|
+
} from '../resource.ts'
|
|
13
|
+
|
|
14
|
+
/** `/v1/lists`. A list holds records of one type; the type is fixed at creation. */
|
|
15
|
+
|
|
16
|
+
const lists = createResourceHooks<List, CreateListInput, ListInput>({
|
|
17
|
+
name: 'lists',
|
|
18
|
+
path: '/lists',
|
|
19
|
+
decode: listSchema.parse,
|
|
20
|
+
createBody: createListBody,
|
|
21
|
+
updateBody: listBody,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export interface ListFilters {
|
|
25
|
+
readonly term?: string | undefined
|
|
26
|
+
readonly targetType?: RecordTargetType | undefined
|
|
27
|
+
readonly limit?: number | undefined
|
|
28
|
+
readonly sort?: string | undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function listsQuery(filters: ListFilters): QueryParameters {
|
|
32
|
+
return {
|
|
33
|
+
q: filters.term,
|
|
34
|
+
target_type: filters.targetType,
|
|
35
|
+
limit: filters.limit,
|
|
36
|
+
sort: filters.sort,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function useLists(
|
|
41
|
+
filters: ListFilters = {},
|
|
42
|
+
options: ListOptions = {},
|
|
43
|
+
): RecordListResult<List> {
|
|
44
|
+
return lists.useList(listsQuery(filters), options)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function useList(id: string | undefined): RecordResult<List> {
|
|
48
|
+
return lists.useRecord(id)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function useCreateList(): MutationResult<CreateListInput, List> {
|
|
52
|
+
return lists.useCreate()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function useUpdateList(): MutationResult<UpdateArguments<ListInput>, List> {
|
|
56
|
+
return lists.useUpdate()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function useDeleteList(): MutationResult<string, void> {
|
|
60
|
+
return lists.useRemove()
|
|
61
|
+
}
|
|
@@ -25,7 +25,10 @@ const people = createResourceHooks<Person, CreatePersonInput, PersonInput>({
|
|
|
25
25
|
updateBody: personBody,
|
|
26
26
|
// Every write here emits an activity server-side, in the same transaction, so
|
|
27
27
|
// a timeline rendered on this record is stale the moment the write lands.
|
|
28
|
-
|
|
28
|
+
// Setting or changing the email may also auto-link a Position, which changes
|
|
29
|
+
// both the person's position list and the `?person_id=` companies list that
|
|
30
|
+
// names the far side — without the latter, the new row renders as "Unknown".
|
|
31
|
+
alsoInvalidates: ['activities', 'positions', 'companies'],
|
|
29
32
|
})
|
|
30
33
|
|
|
31
34
|
/** The documented filters on `GET /v1/people`. There is no generic filter DSL. */
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { publicConfigSchema } from '@kelpie/schemas'
|
|
2
|
+
import type { PublicConfig } from '@kelpie/schemas'
|
|
3
|
+
import { useQuery } from '@tanstack/react-query'
|
|
4
|
+
|
|
5
|
+
import { useApiClient } from '../context.ts'
|
|
6
|
+
import { toError } from '../errors.ts'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The deployment metadata `GET /v1/public/config` reports.
|
|
10
|
+
*
|
|
11
|
+
* Read once, cached forever: neither `runtimeMode` nor `siteName` changes
|
|
12
|
+
* while the app is open. The endpoint is public, so this hook works before a
|
|
13
|
+
* session exists (the sign-in page reads it too).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const PUBLIC_CONFIG_KEY = ['public-config'] as const
|
|
17
|
+
|
|
18
|
+
export interface PublicConfigState {
|
|
19
|
+
readonly config: PublicConfig | undefined
|
|
20
|
+
readonly isLoading: boolean
|
|
21
|
+
readonly error: Error | null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function usePublicConfig(): PublicConfigState {
|
|
25
|
+
const client = useApiClient()
|
|
26
|
+
const result = useQuery({
|
|
27
|
+
queryKey: PUBLIC_CONFIG_KEY,
|
|
28
|
+
queryFn: () => client.get('/public/config', publicConfigSchema.parse),
|
|
29
|
+
staleTime: Infinity,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
return { config: result.data, isLoading: result.isPending, error: toError(result.error) }
|
|
33
|
+
}
|
|
@@ -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,56 @@
|
|
|
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
|
+
'roles',
|
|
49
|
+
'candidates',
|
|
50
|
+
].map((name) => cache.invalidateQueries({ queryKey: [name] })),
|
|
51
|
+
)
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
return asMutationResult(mutation)
|
|
56
|
+
}
|
|
@@ -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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BrowserRouter, Navigate, Route, Routes } from 'react-router'
|
|
2
2
|
|
|
3
3
|
import { ApiProvider } from '../api/ApiProvider.tsx'
|
|
4
|
+
import { EnvironmentBanner } from '../components/EnvironmentBanner.tsx'
|
|
4
5
|
import { Shell } from '../components/Shell.tsx'
|
|
5
6
|
import { CompaniesPage } from '../pages/CompaniesPage.tsx'
|
|
6
7
|
import { CompanyDetail } from '../pages/CompanyDetail.tsx'
|
|
@@ -13,6 +14,8 @@ import { FormDetail } from '../pages/FormDetail.tsx'
|
|
|
13
14
|
import { FormsPage } from '../pages/FormsPage.tsx'
|
|
14
15
|
import { FundraisingPage } from '../pages/FundraisingPage.tsx'
|
|
15
16
|
import { HiringPage } from '../pages/HiringPage.tsx'
|
|
17
|
+
import { ListDetail } from '../pages/ListDetail.tsx'
|
|
18
|
+
import { ListsPage } from '../pages/ListsPage.tsx'
|
|
16
19
|
import { NotFoundPage } from '../pages/NotFoundPage.tsx'
|
|
17
20
|
import { OpportunitiesPage } from '../pages/OpportunitiesPage.tsx'
|
|
18
21
|
import { OpportunityDetail } from '../pages/OpportunityDetail.tsx'
|
|
@@ -78,6 +81,12 @@ export function KelpieApp({ extensions, baseUrl }: KelpieAppProps): React.JSX.El
|
|
|
78
81
|
<ApiProvider baseUrl={baseUrl}>
|
|
79
82
|
<UiExtensionProvider extensions={extensions}>
|
|
80
83
|
<BrowserRouter>
|
|
84
|
+
{/* Above the router so the strip renders on the sign-in page as
|
|
85
|
+
well as the main app. Nothing forces a height here: on
|
|
86
|
+
production the banner returns null, and the Shell (or the sign-
|
|
87
|
+
in layout) fills the viewport as it did before. On non-
|
|
88
|
+
production the 20px strip stacks above them. */}
|
|
89
|
+
<EnvironmentBanner />
|
|
81
90
|
<AppRoutes />
|
|
82
91
|
</BrowserRouter>
|
|
83
92
|
</UiExtensionProvider>
|
|
@@ -152,6 +161,8 @@ function AppRoutes(): React.JSX.Element {
|
|
|
152
161
|
<Route path="partnerships" element={<PartnershipsPage />} />
|
|
153
162
|
<Route path="partnerships/settings" element={<PartnershipStageSettingsPage />} />
|
|
154
163
|
<Route path="partnerships/:id" element={<PartnershipDetail />} />
|
|
164
|
+
<Route path="lists" element={<ListsPage />} />
|
|
165
|
+
<Route path="lists/:id" element={<ListDetail />} />
|
|
155
166
|
<Route path="planning" element={<PlanningPage />} />
|
|
156
167
|
<Route path="decisions" element={<DecisionsPage />} />
|
|
157
168
|
<Route path="forms" element={<FormsPage />} />
|
|
@@ -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
|
|
|
@@ -118,16 +119,7 @@ export function ActivitiesPanel({
|
|
|
118
119
|
))}
|
|
119
120
|
</div>
|
|
120
121
|
|
|
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
|
-
)}
|
|
122
|
+
<Paginator list={activities} />
|
|
131
123
|
</section>
|
|
132
124
|
)
|
|
133
125
|
}
|
|
@@ -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
|
+
}
|