@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
package/src/pages/HiringPage.tsx
CHANGED
|
@@ -6,13 +6,21 @@ import { useNavigate } from 'react-router'
|
|
|
6
6
|
import { useTimezone } from '../api/resources/account.ts'
|
|
7
7
|
import { useCreateRole, useRoles } from '../api/resources/roles.ts'
|
|
8
8
|
import { Chip } from '../components/Chip.tsx'
|
|
9
|
+
import { ColumnPicker } from '../components/ColumnPicker.tsx'
|
|
9
10
|
import { DataTable } from '../components/DataTable.tsx'
|
|
10
11
|
import type { Column } from '../components/DataTable.tsx'
|
|
11
12
|
import { FilterBar, PageHeader } from '../components/PageHeader.tsx'
|
|
13
|
+
import { Paginator } from '../components/Paginator.tsx'
|
|
12
14
|
import { ErrorPanel, LoadingPanel } from '../components/QueryState.tsx'
|
|
13
15
|
import { formatDate } from '../lib/dates.ts'
|
|
16
|
+
import { useListView } from '../lib/listView.ts'
|
|
17
|
+
import { serverSortOnly } from '../lib/sort.ts'
|
|
14
18
|
import { useRoleCandidateCounts } from './hiringDirectory.ts'
|
|
15
19
|
|
|
20
|
+
const DEFAULT_VISIBLE_KEYS: readonly string[] = ['title', 'status', 'candidates', 'created']
|
|
21
|
+
|
|
22
|
+
const SERVER_SORT_KEYS: readonly string[] = ['title', 'created_at', 'updated_at']
|
|
23
|
+
|
|
16
24
|
/**
|
|
17
25
|
* The Hiring list: every opening, open or closed.
|
|
18
26
|
*
|
|
@@ -24,8 +32,12 @@ export function HiringPage(): React.JSX.Element {
|
|
|
24
32
|
const [term, setTerm] = useState('')
|
|
25
33
|
const [adding, setAdding] = useState(false)
|
|
26
34
|
const [title, setTitle] = useState('')
|
|
35
|
+
const [sort, setSort] = useState<string | undefined>(undefined)
|
|
27
36
|
const navigate = useNavigate()
|
|
28
|
-
const roles = useRoles({
|
|
37
|
+
const roles = useRoles({
|
|
38
|
+
term: term.trim().length > 0 ? term.trim() : undefined,
|
|
39
|
+
sort: serverSortOnly(sort, SERVER_SORT_KEYS),
|
|
40
|
+
})
|
|
29
41
|
const counts = useRoleCandidateCounts(roles.records.map((role) => role.id))
|
|
30
42
|
const createRole = useCreateRole()
|
|
31
43
|
const timezone = useTimezone()
|
|
@@ -54,11 +66,13 @@ export function HiringPage(): React.JSX.Element {
|
|
|
54
66
|
{
|
|
55
67
|
key: 'title',
|
|
56
68
|
header: 'Role',
|
|
69
|
+
sortKey: 'title',
|
|
57
70
|
render: (role) => <span className="font-medium text-ink">{role.title}</span>,
|
|
58
71
|
},
|
|
59
72
|
{
|
|
60
73
|
key: 'status',
|
|
61
74
|
header: 'Status',
|
|
75
|
+
getSortValue: (role) => role.status,
|
|
62
76
|
render: (role) => (
|
|
63
77
|
<Chip tone={role.status === 'open' ? 'accent' : 'neutral'}>
|
|
64
78
|
{role.status === 'open' ? 'Open' : 'Closed'}
|
|
@@ -69,6 +83,7 @@ export function HiringPage(): React.JSX.Element {
|
|
|
69
83
|
key: 'candidates',
|
|
70
84
|
header: 'Candidates',
|
|
71
85
|
className: 'w-28',
|
|
86
|
+
getSortValue: (role) => (counts.isLoading ? null : counts.countFor(role.id)),
|
|
72
87
|
render: (role) => (
|
|
73
88
|
<span className="font-mono text-[12px]">
|
|
74
89
|
{counts.isLoading ? '—' : counts.countFor(role.id)}
|
|
@@ -78,10 +93,21 @@ export function HiringPage(): React.JSX.Element {
|
|
|
78
93
|
{
|
|
79
94
|
key: 'created',
|
|
80
95
|
header: 'Created',
|
|
96
|
+
sortKey: 'created_at',
|
|
81
97
|
render: (role) => formatDate(role.createdAt, timezone),
|
|
82
98
|
},
|
|
99
|
+
{
|
|
100
|
+
key: 'updated',
|
|
101
|
+
header: 'Updated',
|
|
102
|
+
sortKey: 'updated_at',
|
|
103
|
+
render: (role) => formatDate(role.updatedAt, timezone),
|
|
104
|
+
},
|
|
83
105
|
]
|
|
84
106
|
|
|
107
|
+
const supportedKeys = columns.map((column) => column.key)
|
|
108
|
+
const listView = useListView('roles', supportedKeys, DEFAULT_VISIBLE_KEYS)
|
|
109
|
+
const pickerOptions = columns.map((column) => ({ key: column.key, label: column.header }))
|
|
110
|
+
|
|
85
111
|
return (
|
|
86
112
|
<div className="animate-fade-in">
|
|
87
113
|
<PageHeader
|
|
@@ -90,6 +116,13 @@ export function HiringPage(): React.JSX.Element {
|
|
|
90
116
|
setAdding((current) => !current)
|
|
91
117
|
}}
|
|
92
118
|
addLabel="Add role"
|
|
119
|
+
actions={
|
|
120
|
+
<ColumnPicker
|
|
121
|
+
options={pickerOptions}
|
|
122
|
+
visibleKeys={listView.visibleKeys}
|
|
123
|
+
onChange={listView.setVisibleKeys}
|
|
124
|
+
/>
|
|
125
|
+
}
|
|
93
126
|
/>
|
|
94
127
|
|
|
95
128
|
{adding && (
|
|
@@ -140,6 +173,7 @@ export function HiringPage(): React.JSX.Element {
|
|
|
140
173
|
<LoadingPanel label="Loading roles…" />
|
|
141
174
|
) : (
|
|
142
175
|
<>
|
|
176
|
+
<Paginator list={roles} placement="top" />
|
|
143
177
|
<DataTable
|
|
144
178
|
columns={columns}
|
|
145
179
|
rows={roles.records}
|
|
@@ -148,22 +182,16 @@ export function HiringPage(): React.JSX.Element {
|
|
|
148
182
|
void navigate(`/hiring/${role.id}`)
|
|
149
183
|
}}
|
|
150
184
|
emptyMessage="No roles yet"
|
|
185
|
+
sort={sort}
|
|
186
|
+
onSortChange={setSort}
|
|
187
|
+
visibleColumnKeys={listView.visibleKeys}
|
|
151
188
|
/>
|
|
152
189
|
{!counts.isComplete && !counts.isLoading && (
|
|
153
190
|
<p className="mt-2 text-[11px] text-ink-faint">
|
|
154
191
|
These roles hold more candidates than one page returns, so some counts read low.
|
|
155
192
|
</p>
|
|
156
193
|
)}
|
|
157
|
-
{roles
|
|
158
|
-
<button
|
|
159
|
-
type="button"
|
|
160
|
-
onClick={roles.loadMore}
|
|
161
|
-
disabled={roles.isLoadingMore}
|
|
162
|
-
className="mt-3 rounded-md border border-border px-3 py-1.5 text-[12px] font-medium text-ink transition hover:border-border-strong hover:bg-surface-sunken disabled:opacity-50"
|
|
163
|
-
>
|
|
164
|
-
{roles.isLoadingMore ? 'Loading…' : 'Load more'}
|
|
165
|
-
</button>
|
|
166
|
-
)}
|
|
194
|
+
<Paginator list={roles} />
|
|
167
195
|
</>
|
|
168
196
|
)}
|
|
169
197
|
</div>
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import { RECORD_TARGET_TYPE_LABELS } from '@kelpie/schemas'
|
|
2
|
+
import type { ListMember, RecordTargetType } from '@kelpie/schemas'
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { Link, useNavigate, useParams } from 'react-router'
|
|
5
|
+
|
|
6
|
+
import { usePatch } from '../api/resource.ts'
|
|
7
|
+
import type { PatchResult } from '../api/resource.ts'
|
|
8
|
+
import { useCompanies } from '../api/resources/companies.ts'
|
|
9
|
+
import { useDeals } from '../api/resources/deals.ts'
|
|
10
|
+
import { useDeleteList, useList, useUpdateList } from '../api/resources/lists.ts'
|
|
11
|
+
import {
|
|
12
|
+
useAddListMember,
|
|
13
|
+
useListMembers,
|
|
14
|
+
useRemoveListMember,
|
|
15
|
+
} from '../api/resources/listMembers.ts'
|
|
16
|
+
import { useOpportunities } from '../api/resources/opportunities.ts'
|
|
17
|
+
import { usePartnerships } from '../api/resources/partnerships.ts'
|
|
18
|
+
import { usePeople } from '../api/resources/people.ts'
|
|
19
|
+
import { useRaises } from '../api/resources/raises.ts'
|
|
20
|
+
import { Chip } from '../components/Chip.tsx'
|
|
21
|
+
import { DeleteRecord } from '../components/DeleteRecord.tsx'
|
|
22
|
+
import { EntitySearch } from '../components/EntitySearch.tsx'
|
|
23
|
+
import type { SearchOption } from '../components/EntitySearch.tsx'
|
|
24
|
+
import { InlineEdit } from '../components/InlineEdit.tsx'
|
|
25
|
+
import { Paginator } from '../components/Paginator.tsx'
|
|
26
|
+
import { ErrorPanel, LoadingPanel, NotFoundPanel } from '../components/QueryState.tsx'
|
|
27
|
+
import { SectionHeader } from '../components/SectionHeader.tsx'
|
|
28
|
+
import type { List, ListInput } from '@kelpie/schemas'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* One list, and the records on it.
|
|
32
|
+
*
|
|
33
|
+
* The type is fixed at creation, so the header shows it as a badge and the
|
|
34
|
+
* member picker below dispatches on it: a `person` list picks from people, a
|
|
35
|
+
* `company` list from companies, and so on. A candidate list has no picker in
|
|
36
|
+
* v1 because candidates carry no name of their own; add candidacies from the
|
|
37
|
+
* hiring pages.
|
|
38
|
+
*/
|
|
39
|
+
export function ListDetail(): React.JSX.Element {
|
|
40
|
+
const { id } = useParams()
|
|
41
|
+
const navigate = useNavigate()
|
|
42
|
+
const { record, isLoading, isNotFound, error } = useList(id)
|
|
43
|
+
const deleteList = useDeleteList()
|
|
44
|
+
|
|
45
|
+
if (isNotFound) {
|
|
46
|
+
return <NotFoundPanel label="List" backTo="/lists" />
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (error !== null) {
|
|
50
|
+
return <ErrorPanel error={error} />
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (isLoading || record === undefined || id === undefined) {
|
|
54
|
+
return <LoadingPanel label="Loading list…" />
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="animate-fade-in mx-auto max-w-4xl">
|
|
59
|
+
<Link
|
|
60
|
+
to="/lists"
|
|
61
|
+
className="mb-4 inline-flex text-[12px] font-medium text-ink-muted transition hover:text-accent"
|
|
62
|
+
>
|
|
63
|
+
← Lists
|
|
64
|
+
</Link>
|
|
65
|
+
|
|
66
|
+
<div className="mb-6 flex flex-wrap items-start justify-between gap-3">
|
|
67
|
+
<ListHeading list={record} />
|
|
68
|
+
<div className="flex shrink-0 flex-wrap items-center gap-3">
|
|
69
|
+
<Chip>{RECORD_TARGET_TYPE_LABELS[record.targetType]}</Chip>
|
|
70
|
+
<DeleteRecord
|
|
71
|
+
recordLabel="List"
|
|
72
|
+
recordName={record.name}
|
|
73
|
+
isPending={deleteList.isPending}
|
|
74
|
+
error={deleteList.error}
|
|
75
|
+
onConfirm={() => {
|
|
76
|
+
deleteList
|
|
77
|
+
.runAsync(record.id)
|
|
78
|
+
.then(() => navigate('/lists'))
|
|
79
|
+
.catch(() => undefined)
|
|
80
|
+
}}
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<ListDescription list={record} />
|
|
86
|
+
|
|
87
|
+
<ListMembers listId={record.id} targetType={record.targetType} />
|
|
88
|
+
</div>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function useListPatch(list: List): PatchResult<ListInput> {
|
|
93
|
+
return usePatch(useUpdateList, list)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function ListHeading({ list }: { readonly list: List }): React.JSX.Element {
|
|
97
|
+
const { patch, error } = useListPatch(list)
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div className="min-w-0 flex-1">
|
|
101
|
+
{error !== null && (
|
|
102
|
+
<div className="mb-2">
|
|
103
|
+
<ErrorPanel error={error} />
|
|
104
|
+
</div>
|
|
105
|
+
)}
|
|
106
|
+
<InlineEdit
|
|
107
|
+
value={list.name}
|
|
108
|
+
onChange={(name) => {
|
|
109
|
+
patch({ name })
|
|
110
|
+
}}
|
|
111
|
+
displayClassName="text-[22px] font-semibold tracking-tight text-ink not-italic"
|
|
112
|
+
emptyLabel="Untitled list"
|
|
113
|
+
/>
|
|
114
|
+
</div>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function ListDescription({ list }: { readonly list: List }): React.JSX.Element {
|
|
119
|
+
const { patch, error } = useListPatch(list)
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div className="mb-6">
|
|
123
|
+
{error !== null && (
|
|
124
|
+
<div className="mb-2">
|
|
125
|
+
<ErrorPanel error={error} />
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
128
|
+
<InlineEdit
|
|
129
|
+
value={list.description ?? ''}
|
|
130
|
+
onChange={(description) => {
|
|
131
|
+
patch({ description: description.length > 0 ? description : null })
|
|
132
|
+
}}
|
|
133
|
+
displayClassName="text-[13px] text-ink-muted not-italic"
|
|
134
|
+
emptyLabel="Add a description"
|
|
135
|
+
multiline
|
|
136
|
+
/>
|
|
137
|
+
</div>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function ListMembers({
|
|
142
|
+
listId,
|
|
143
|
+
targetType,
|
|
144
|
+
}: {
|
|
145
|
+
readonly listId: string
|
|
146
|
+
readonly targetType: RecordTargetType
|
|
147
|
+
}): React.JSX.Element {
|
|
148
|
+
const members = useListMembers(listId)
|
|
149
|
+
const addMember = useAddListMember()
|
|
150
|
+
const [adding, setAdding] = useState(false)
|
|
151
|
+
|
|
152
|
+
const attached = new Set(members.records.map((member) => member.targetId))
|
|
153
|
+
|
|
154
|
+
function add(targetId: string): void {
|
|
155
|
+
if (attached.has(targetId)) {
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
addMember.run({ listId, input: { targetType, targetId } })
|
|
160
|
+
setAdding(false)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<section>
|
|
165
|
+
<SectionHeader
|
|
166
|
+
title="Members"
|
|
167
|
+
onAdd={() => {
|
|
168
|
+
setAdding((current) => !current)
|
|
169
|
+
}}
|
|
170
|
+
addLabel={`Add ${RECORD_TARGET_TYPE_LABELS[targetType].toLowerCase()}`}
|
|
171
|
+
/>
|
|
172
|
+
|
|
173
|
+
{adding && (
|
|
174
|
+
<div className="mb-3 flex gap-2">
|
|
175
|
+
<div className="min-w-0 flex-1">
|
|
176
|
+
<MemberPicker
|
|
177
|
+
targetType={targetType}
|
|
178
|
+
attached={attached}
|
|
179
|
+
onPick={add}
|
|
180
|
+
/>
|
|
181
|
+
</div>
|
|
182
|
+
<button
|
|
183
|
+
type="button"
|
|
184
|
+
onClick={() => {
|
|
185
|
+
setAdding(false)
|
|
186
|
+
}}
|
|
187
|
+
className="shrink-0 rounded-md px-2.5 py-1.5 text-[12px] font-medium text-ink-muted hover:text-ink"
|
|
188
|
+
>
|
|
189
|
+
Cancel
|
|
190
|
+
</button>
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
|
|
194
|
+
{addMember.error !== null && (
|
|
195
|
+
<div className="mb-3">
|
|
196
|
+
<ErrorPanel error={addMember.error} />
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
{members.error !== null && <ErrorPanel error={members.error} />}
|
|
200
|
+
|
|
201
|
+
{members.isLoading ? (
|
|
202
|
+
<LoadingPanel label="Loading members…" />
|
|
203
|
+
) : members.records.length === 0 && !adding ? (
|
|
204
|
+
<p className="text-[13px] text-ink-faint">No members yet.</p>
|
|
205
|
+
) : (
|
|
206
|
+
<>
|
|
207
|
+
<Paginator list={members} placement="top" />
|
|
208
|
+
<ul className="overflow-hidden rounded-md border border-border">
|
|
209
|
+
{members.records.map((member) => (
|
|
210
|
+
<MemberRow key={member.id} listId={listId} member={member} />
|
|
211
|
+
))}
|
|
212
|
+
</ul>
|
|
213
|
+
</>
|
|
214
|
+
)}
|
|
215
|
+
|
|
216
|
+
<Paginator list={members} />
|
|
217
|
+
</section>
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function MemberRow({
|
|
222
|
+
listId,
|
|
223
|
+
member,
|
|
224
|
+
}: {
|
|
225
|
+
readonly listId: string
|
|
226
|
+
readonly member: ListMember
|
|
227
|
+
}): React.JSX.Element {
|
|
228
|
+
const remove = useRemoveListMember()
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<li className="border-b border-border px-4 py-3 last:border-0">
|
|
232
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
233
|
+
<Link
|
|
234
|
+
to={detailPathFor(member.targetType, member.targetId)}
|
|
235
|
+
className="text-left text-[13px] font-medium text-ink hover:text-accent"
|
|
236
|
+
>
|
|
237
|
+
{member.targetName ?? member.targetId}
|
|
238
|
+
</Link>
|
|
239
|
+
<button
|
|
240
|
+
type="button"
|
|
241
|
+
onClick={() => {
|
|
242
|
+
remove.run({ listId, id: member.id })
|
|
243
|
+
}}
|
|
244
|
+
className="text-[11px] font-medium text-danger hover:underline"
|
|
245
|
+
>
|
|
246
|
+
Remove
|
|
247
|
+
</button>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
{remove.error !== null && (
|
|
251
|
+
<div className="mt-2">
|
|
252
|
+
<ErrorPanel error={remove.error} />
|
|
253
|
+
</div>
|
|
254
|
+
)}
|
|
255
|
+
</li>
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Where a link on a member row should go. Every type has its own detail path. */
|
|
260
|
+
function detailPathFor(targetType: RecordTargetType, targetId: string): string {
|
|
261
|
+
switch (targetType) {
|
|
262
|
+
case 'person':
|
|
263
|
+
return `/people/${targetId}`
|
|
264
|
+
case 'company':
|
|
265
|
+
return `/companies/${targetId}`
|
|
266
|
+
case 'deal':
|
|
267
|
+
return `/deals/${targetId}`
|
|
268
|
+
case 'opportunity':
|
|
269
|
+
return `/opportunities/${targetId}`
|
|
270
|
+
case 'partnership':
|
|
271
|
+
return `/partnerships/${targetId}`
|
|
272
|
+
case 'raise':
|
|
273
|
+
return `/fundraising/${targetId}`
|
|
274
|
+
case 'candidate':
|
|
275
|
+
// A candidate has no page of its own; the person's does.
|
|
276
|
+
return `/hiring`
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* A "search or pick" combobox that queries the collection matching the list's
|
|
282
|
+
* type. Each type gets its own component so React sees a stable set of hook
|
|
283
|
+
* calls per render.
|
|
284
|
+
*/
|
|
285
|
+
function MemberPicker({
|
|
286
|
+
targetType,
|
|
287
|
+
attached,
|
|
288
|
+
onPick,
|
|
289
|
+
}: {
|
|
290
|
+
readonly targetType: RecordTargetType
|
|
291
|
+
readonly attached: ReadonlySet<string>
|
|
292
|
+
readonly onPick: (id: string) => void
|
|
293
|
+
}): React.JSX.Element {
|
|
294
|
+
switch (targetType) {
|
|
295
|
+
case 'person':
|
|
296
|
+
return <PeoplePicker attached={attached} onPick={onPick} />
|
|
297
|
+
case 'company':
|
|
298
|
+
return <CompaniesPicker attached={attached} onPick={onPick} />
|
|
299
|
+
case 'deal':
|
|
300
|
+
return <DealsPicker attached={attached} onPick={onPick} />
|
|
301
|
+
case 'opportunity':
|
|
302
|
+
return <OpportunitiesPicker attached={attached} onPick={onPick} />
|
|
303
|
+
case 'partnership':
|
|
304
|
+
return <PartnershipsPicker attached={attached} onPick={onPick} />
|
|
305
|
+
case 'raise':
|
|
306
|
+
return <RaisesPicker attached={attached} onPick={onPick} />
|
|
307
|
+
case 'candidate':
|
|
308
|
+
return (
|
|
309
|
+
<p className="text-[12px] text-ink-faint">
|
|
310
|
+
Candidate lists have no inline picker in this release. Add candidacies from a role's
|
|
311
|
+
page, then attach them here through the API.
|
|
312
|
+
</p>
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
interface PickerProps {
|
|
318
|
+
readonly attached: ReadonlySet<string>
|
|
319
|
+
readonly onPick: (id: string) => void
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function PeoplePicker({ attached, onPick }: PickerProps): React.JSX.Element {
|
|
323
|
+
const [search, setSearch] = useState('')
|
|
324
|
+
const records = usePeople({ term: trimmedOrUndefined(search) })
|
|
325
|
+
const options: readonly SearchOption[] = records.records
|
|
326
|
+
.filter((row) => !attached.has(row.id))
|
|
327
|
+
.map((row) => ({ id: row.id, label: row.name, meta: row.email ?? undefined }))
|
|
328
|
+
|
|
329
|
+
return <PickerBox onQueryChange={setSearch} onPick={onPick} options={options} />
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function CompaniesPicker({ attached, onPick }: PickerProps): React.JSX.Element {
|
|
333
|
+
const [search, setSearch] = useState('')
|
|
334
|
+
const records = useCompanies({ term: trimmedOrUndefined(search) })
|
|
335
|
+
const options: readonly SearchOption[] = records.records
|
|
336
|
+
.filter((row) => !attached.has(row.id))
|
|
337
|
+
.map((row) => ({ id: row.id, label: row.name, meta: row.domain ?? undefined }))
|
|
338
|
+
|
|
339
|
+
return <PickerBox onQueryChange={setSearch} onPick={onPick} options={options} />
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function DealsPicker({ attached, onPick }: PickerProps): React.JSX.Element {
|
|
343
|
+
const [search, setSearch] = useState('')
|
|
344
|
+
const records = useDeals({ term: trimmedOrUndefined(search) })
|
|
345
|
+
const options: readonly SearchOption[] = records.records
|
|
346
|
+
.filter((row) => !attached.has(row.id))
|
|
347
|
+
.map((row) => ({ id: row.id, label: row.name }))
|
|
348
|
+
|
|
349
|
+
return <PickerBox onQueryChange={setSearch} onPick={onPick} options={options} />
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function OpportunitiesPicker({ attached, onPick }: PickerProps): React.JSX.Element {
|
|
353
|
+
const [search, setSearch] = useState('')
|
|
354
|
+
const records = useOpportunities({ term: trimmedOrUndefined(search) })
|
|
355
|
+
const options: readonly SearchOption[] = records.records
|
|
356
|
+
.filter((row) => !attached.has(row.id))
|
|
357
|
+
.map((row) => ({ id: row.id, label: row.name }))
|
|
358
|
+
|
|
359
|
+
return <PickerBox onQueryChange={setSearch} onPick={onPick} options={options} />
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function PartnershipsPicker({ attached, onPick }: PickerProps): React.JSX.Element {
|
|
363
|
+
const [search, setSearch] = useState('')
|
|
364
|
+
const records = usePartnerships({ term: trimmedOrUndefined(search) })
|
|
365
|
+
const options: readonly SearchOption[] = records.records
|
|
366
|
+
.filter((row) => !attached.has(row.id))
|
|
367
|
+
.map((row) => ({ id: row.id, label: row.name }))
|
|
368
|
+
|
|
369
|
+
return <PickerBox onQueryChange={setSearch} onPick={onPick} options={options} />
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function RaisesPicker({ attached, onPick }: PickerProps): React.JSX.Element {
|
|
373
|
+
const [search, setSearch] = useState('')
|
|
374
|
+
const records = useRaises({ term: trimmedOrUndefined(search) })
|
|
375
|
+
const options: readonly SearchOption[] = records.records
|
|
376
|
+
.filter((row) => !attached.has(row.id))
|
|
377
|
+
.map((row) => ({ id: row.id, label: row.name }))
|
|
378
|
+
|
|
379
|
+
return <PickerBox onQueryChange={setSearch} onPick={onPick} options={options} />
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function PickerBox({
|
|
383
|
+
onQueryChange,
|
|
384
|
+
onPick,
|
|
385
|
+
options,
|
|
386
|
+
}: {
|
|
387
|
+
readonly onQueryChange: (value: string) => void
|
|
388
|
+
readonly onPick: (id: string) => void
|
|
389
|
+
readonly options: readonly SearchOption[]
|
|
390
|
+
}): React.JSX.Element {
|
|
391
|
+
return (
|
|
392
|
+
<EntitySearch
|
|
393
|
+
options={options}
|
|
394
|
+
value=""
|
|
395
|
+
onChange={onPick}
|
|
396
|
+
onQueryChange={onQueryChange}
|
|
397
|
+
placeholder="Search…"
|
|
398
|
+
emptyMessage="No matches"
|
|
399
|
+
size="md"
|
|
400
|
+
/>
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function trimmedOrUndefined(value: string): string | undefined {
|
|
405
|
+
const trimmed = value.trim()
|
|
406
|
+
|
|
407
|
+
return trimmed.length > 0 ? trimmed : undefined
|
|
408
|
+
}
|