@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
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Partnership, PipelineStage } from '@kelpie/schemas'
|
|
2
|
-
import { useState } from 'react'
|
|
3
2
|
import { Link, useNavigate } from 'react-router'
|
|
4
3
|
|
|
5
4
|
import { useCompanies } from '../api/resources/companies.ts'
|
|
@@ -11,16 +10,21 @@ import {
|
|
|
11
10
|
} from '../api/resources/partnerships.ts'
|
|
12
11
|
import { usePipelineStages } from '../api/resources/pipelineStages.ts'
|
|
13
12
|
import { MAX_PAGE_SIZE, usePlanItems } from '../api/resources/planItems.ts'
|
|
13
|
+
import { useTimezone } from '../api/resources/account.ts'
|
|
14
14
|
import { Chip } from '../components/Chip.tsx'
|
|
15
15
|
import type { ChipTone } from '../components/Chip.tsx'
|
|
16
|
+
import { ColumnPicker } from '../components/ColumnPicker.tsx'
|
|
16
17
|
import { DataTable } from '../components/DataTable.tsx'
|
|
17
18
|
import type { Column, DataTableGroup } from '../components/DataTable.tsx'
|
|
18
19
|
import { KanbanBoard } from '../components/KanbanBoard.tsx'
|
|
19
20
|
import { PageHeader } from '../components/PageHeader.tsx'
|
|
21
|
+
import { Paginator } from '../components/Paginator.tsx'
|
|
20
22
|
import { ErrorPanel, LoadingPanel } from '../components/QueryState.tsx'
|
|
21
23
|
import { SegmentedControl } from '../components/SegmentedControl.tsx'
|
|
22
|
-
import { formatDay } from '../lib/dates.ts'
|
|
24
|
+
import { formatDate, formatDay } from '../lib/dates.ts'
|
|
25
|
+
import { useListView } from '../lib/listView.ts'
|
|
23
26
|
import { DUE_BUCKETS, byDateThenTitle, dueBucketFor, nextOpenByTarget } from '../lib/plan.ts'
|
|
27
|
+
import { serverSortOnly } from '../lib/sort.ts'
|
|
24
28
|
|
|
25
29
|
/**
|
|
26
30
|
* The Partnerships board: ongoing two-way relationships, grouped by status. The
|
|
@@ -32,6 +36,33 @@ type BoardView = 'list' | 'columns'
|
|
|
32
36
|
type PipelineScope = 'open' | 'all'
|
|
33
37
|
type ListGrouping = 'stage' | 'due'
|
|
34
38
|
|
|
39
|
+
const SCOPES: readonly PipelineScope[] = ['open', 'all']
|
|
40
|
+
const GROUPINGS: readonly ListGrouping[] = ['stage', 'due']
|
|
41
|
+
|
|
42
|
+
function readScope(stored: string | undefined): PipelineScope {
|
|
43
|
+
return SCOPES.includes(stored as PipelineScope) ? (stored as PipelineScope) : 'open'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readGrouping(stored: string | undefined): ListGrouping {
|
|
47
|
+
return GROUPINGS.includes(stored as ListGrouping) ? (stored as ListGrouping) : 'stage'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const COLUMN_KEYS: readonly string[] = [
|
|
51
|
+
'name',
|
|
52
|
+
'company',
|
|
53
|
+
'stage',
|
|
54
|
+
'kind',
|
|
55
|
+
'nextPlan',
|
|
56
|
+
'owner',
|
|
57
|
+
'next',
|
|
58
|
+
'goals',
|
|
59
|
+
'successLooksLike',
|
|
60
|
+
'tags',
|
|
61
|
+
'summary',
|
|
62
|
+
'createdAt',
|
|
63
|
+
'updatedAt',
|
|
64
|
+
]
|
|
65
|
+
|
|
35
66
|
/** Tones keyed by the seeded slugs. A renamed stage keeps its slug and its tone. */
|
|
36
67
|
const STAGE_TONES: Readonly<Record<string, ChipTone>> = {
|
|
37
68
|
active: 'success',
|
|
@@ -43,19 +74,33 @@ function stageTone(stage: PipelineStage): ChipTone {
|
|
|
43
74
|
return STAGE_TONES[stage.slug] ?? 'neutral'
|
|
44
75
|
}
|
|
45
76
|
|
|
77
|
+
const DEFAULT_VISIBLE_KEYS: readonly string[] = [
|
|
78
|
+
'name',
|
|
79
|
+
'company',
|
|
80
|
+
'kind',
|
|
81
|
+
'nextPlan',
|
|
82
|
+
'owner',
|
|
83
|
+
'next',
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
const SERVER_SORT_KEYS: readonly string[] = ['name', 'created_at', 'updated_at']
|
|
87
|
+
|
|
46
88
|
export function PartnershipsPage(): React.JSX.Element {
|
|
47
89
|
const navigate = useNavigate()
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
90
|
+
|
|
91
|
+
const listView = useListView('partnerships', COLUMN_KEYS, DEFAULT_VISIBLE_KEYS)
|
|
92
|
+
const view: BoardView = listView.mode ?? 'columns'
|
|
93
|
+
const scope = readScope(listView.scope)
|
|
94
|
+
const grouping = readGrouping(listView.grouping)
|
|
95
|
+
const sort = listView.sort
|
|
52
96
|
|
|
53
97
|
const stages = usePipelineStages('partnership')
|
|
54
|
-
const partnerships = usePartnerships({ sort })
|
|
98
|
+
const partnerships = usePartnerships({ sort: serverSortOnly(sort, SERVER_SORT_KEYS) })
|
|
55
99
|
const companies = useCompanies({ limit: 200 })
|
|
56
100
|
const members = useMembers()
|
|
57
101
|
const createPartnership = useCreatePartnership()
|
|
58
102
|
const updatePartnership = useUpdatePartnership()
|
|
103
|
+
const timezone = useTimezone()
|
|
59
104
|
|
|
60
105
|
const allStages = [...stages.records].sort((a, b) => a.sortOrder - b.sortOrder)
|
|
61
106
|
const visibleStages = scope === 'open' ? allStages.filter((stage) => stage.open) : allStages
|
|
@@ -78,7 +123,7 @@ export function PartnershipsPage(): React.JSX.Element {
|
|
|
78
123
|
{ enabled: askedIds.length > 0 },
|
|
79
124
|
)
|
|
80
125
|
const nextPlanByPartnership = nextOpenByTarget(planItems.records)
|
|
81
|
-
const plansTruncated = visiblePartnerships.length > MAX_PAGE_SIZE || planItems.
|
|
126
|
+
const plansTruncated = visiblePartnerships.length > MAX_PAGE_SIZE || planItems.hasNext
|
|
82
127
|
|
|
83
128
|
const companyNameById = new Map(companies.records.map((company) => [company.id, company.name]))
|
|
84
129
|
|
|
@@ -97,6 +142,8 @@ export function PartnershipsPage(): React.JSX.Element {
|
|
|
97
142
|
await navigate(`/partnerships/${partnership.id}`)
|
|
98
143
|
}
|
|
99
144
|
|
|
145
|
+
const stageLabelById = new Map(allStages.map((stage) => [stage.id, stage.label]))
|
|
146
|
+
|
|
100
147
|
const columns: readonly Column<Partnership>[] = [
|
|
101
148
|
{
|
|
102
149
|
key: 'name',
|
|
@@ -107,16 +154,25 @@ export function PartnershipsPage(): React.JSX.Element {
|
|
|
107
154
|
{
|
|
108
155
|
key: 'company',
|
|
109
156
|
header: 'Company',
|
|
157
|
+
getSortValue: (partnership) => companyNameById.get(partnership.companyId) ?? null,
|
|
110
158
|
render: (partnership) => companyNameById.get(partnership.companyId) ?? '—',
|
|
111
159
|
},
|
|
160
|
+
{
|
|
161
|
+
key: 'stage',
|
|
162
|
+
header: 'Stage',
|
|
163
|
+
getSortValue: (partnership) => stageLabelById.get(partnership.stageId) ?? null,
|
|
164
|
+
render: (partnership) => stageLabelById.get(partnership.stageId) ?? '—',
|
|
165
|
+
},
|
|
112
166
|
{
|
|
113
167
|
key: 'kind',
|
|
114
168
|
header: 'Kind',
|
|
169
|
+
getSortValue: (partnership) => partnership.kind || null,
|
|
115
170
|
render: (partnership) => (partnership.kind.length > 0 ? partnership.kind : '—'),
|
|
116
171
|
},
|
|
117
172
|
{
|
|
118
173
|
key: 'nextPlan',
|
|
119
174
|
header: 'Next plan',
|
|
175
|
+
getSortValue: (partnership) => nextPlanByPartnership.get(partnership.id)?.date ?? null,
|
|
120
176
|
render: (partnership) => {
|
|
121
177
|
const next = nextPlanByPartnership.get(partnership.id)
|
|
122
178
|
|
|
@@ -137,6 +193,10 @@ export function PartnershipsPage(): React.JSX.Element {
|
|
|
137
193
|
{
|
|
138
194
|
key: 'owner',
|
|
139
195
|
header: 'Owner',
|
|
196
|
+
getSortValue: (partnership) =>
|
|
197
|
+
partnership.ownerId === null
|
|
198
|
+
? null
|
|
199
|
+
: (members.nameById.get(partnership.ownerId) ?? 'Unknown'),
|
|
140
200
|
render: (partnership) =>
|
|
141
201
|
partnership.ownerId === null
|
|
142
202
|
? '—'
|
|
@@ -146,14 +206,71 @@ export function PartnershipsPage(): React.JSX.Element {
|
|
|
146
206
|
key: 'next',
|
|
147
207
|
header: 'Next touch',
|
|
148
208
|
className: 'w-28',
|
|
209
|
+
getSortValue: (partnership) => partnership.nextTouchpoint,
|
|
149
210
|
render: (partnership) => (
|
|
150
211
|
<span className="font-mono text-[12px] text-ink-muted">
|
|
151
212
|
{partnership.nextTouchpoint === null ? '—' : formatDay(partnership.nextTouchpoint)}
|
|
152
213
|
</span>
|
|
153
214
|
),
|
|
154
215
|
},
|
|
216
|
+
{
|
|
217
|
+
key: 'goals',
|
|
218
|
+
header: 'Goals',
|
|
219
|
+
getSortValue: (partnership) => partnership.goals || null,
|
|
220
|
+
render: (partnership) =>
|
|
221
|
+
partnership.goals.length === 0 ? '—' : (
|
|
222
|
+
<span className="text-ink-muted">{partnership.goals}</span>
|
|
223
|
+
),
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
key: 'successLooksLike',
|
|
227
|
+
header: 'Success looks like',
|
|
228
|
+
getSortValue: (partnership) => partnership.successLooksLike || null,
|
|
229
|
+
render: (partnership) =>
|
|
230
|
+
partnership.successLooksLike.length === 0 ? '—' : (
|
|
231
|
+
<span className="text-ink-muted">{partnership.successLooksLike}</span>
|
|
232
|
+
),
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
key: 'tags',
|
|
236
|
+
header: 'Tags',
|
|
237
|
+
getSortValue: (partnership) => partnership.tags.join(', ') || null,
|
|
238
|
+
render: (partnership) =>
|
|
239
|
+
partnership.tags.length === 0 ? '—' : (
|
|
240
|
+
<span className="flex flex-wrap gap-1">
|
|
241
|
+
{partnership.tags.map((tag) => (
|
|
242
|
+
<Chip key={tag}>
|
|
243
|
+
<span className="text-[10px]">{tag}</span>
|
|
244
|
+
</Chip>
|
|
245
|
+
))}
|
|
246
|
+
</span>
|
|
247
|
+
),
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
key: 'summary',
|
|
251
|
+
header: 'Summary',
|
|
252
|
+
getSortValue: (partnership) => partnership.summary || null,
|
|
253
|
+
render: (partnership) =>
|
|
254
|
+
partnership.summary.length === 0 ? '—' : (
|
|
255
|
+
<span className="text-ink-muted">{partnership.summary}</span>
|
|
256
|
+
),
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
key: 'createdAt',
|
|
260
|
+
header: 'Created',
|
|
261
|
+
sortKey: 'created_at',
|
|
262
|
+
render: (partnership) => formatDate(partnership.createdAt, timezone),
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
key: 'updatedAt',
|
|
266
|
+
header: 'Updated',
|
|
267
|
+
sortKey: 'updated_at',
|
|
268
|
+
render: (partnership) => formatDate(partnership.updatedAt, timezone),
|
|
269
|
+
},
|
|
155
270
|
]
|
|
156
271
|
|
|
272
|
+
const pickerOptions = columns.map((column) => ({ key: column.key, label: column.header }))
|
|
273
|
+
|
|
157
274
|
const stageGroups: readonly DataTableGroup<Partnership>[] = visibleStages.map((stage) => ({
|
|
158
275
|
id: stage.id,
|
|
159
276
|
label: <Chip tone={stageTone(stage)}>{stage.label}</Chip>,
|
|
@@ -217,7 +334,9 @@ export function PartnershipsPage(): React.JSX.Element {
|
|
|
217
334
|
<SegmentedControl
|
|
218
335
|
ariaLabel="Status scope"
|
|
219
336
|
value={scope}
|
|
220
|
-
onChange={
|
|
337
|
+
onChange={(next) => {
|
|
338
|
+
listView.setScope(next)
|
|
339
|
+
}}
|
|
221
340
|
options={[
|
|
222
341
|
{ id: 'open', label: 'Open' },
|
|
223
342
|
{ id: 'all', label: 'All' },
|
|
@@ -227,7 +346,9 @@ export function PartnershipsPage(): React.JSX.Element {
|
|
|
227
346
|
<SegmentedControl
|
|
228
347
|
ariaLabel="Group the list"
|
|
229
348
|
value={grouping}
|
|
230
|
-
onChange={
|
|
349
|
+
onChange={(next) => {
|
|
350
|
+
listView.setGrouping(next)
|
|
351
|
+
}}
|
|
231
352
|
options={[
|
|
232
353
|
{ id: 'stage', label: 'By status' },
|
|
233
354
|
{ id: 'due', label: 'By due' },
|
|
@@ -237,12 +358,21 @@ export function PartnershipsPage(): React.JSX.Element {
|
|
|
237
358
|
<SegmentedControl
|
|
238
359
|
ariaLabel="Board or list"
|
|
239
360
|
value={view}
|
|
240
|
-
onChange={
|
|
361
|
+
onChange={(next) => {
|
|
362
|
+
listView.setMode(next)
|
|
363
|
+
}}
|
|
241
364
|
options={[
|
|
242
365
|
{ id: 'list', label: 'List' },
|
|
243
366
|
{ id: 'columns', label: 'Board' },
|
|
244
367
|
]}
|
|
245
368
|
/>
|
|
369
|
+
{view === 'list' && (
|
|
370
|
+
<ColumnPicker
|
|
371
|
+
options={pickerOptions}
|
|
372
|
+
visibleKeys={listView.visibleKeys}
|
|
373
|
+
onChange={listView.setVisibleKeys}
|
|
374
|
+
/>
|
|
375
|
+
)}
|
|
246
376
|
</>
|
|
247
377
|
}
|
|
248
378
|
/>
|
|
@@ -265,6 +395,7 @@ export function PartnershipsPage(): React.JSX.Element {
|
|
|
265
395
|
<LoadingPanel label="Loading partnerships…" />
|
|
266
396
|
) : (
|
|
267
397
|
<>
|
|
398
|
+
{view === 'list' && <Paginator list={partnerships} placement="top" />}
|
|
268
399
|
{view === 'columns' ? (
|
|
269
400
|
<KanbanBoard
|
|
270
401
|
stages={visibleStages.map((stage) => ({ id: stage.id, label: stage.label }))}
|
|
@@ -311,13 +442,16 @@ export function PartnershipsPage(): React.JSX.Element {
|
|
|
311
442
|
? {
|
|
312
443
|
label: 'Show all partnerships',
|
|
313
444
|
onClick: () => {
|
|
314
|
-
setScope('all')
|
|
445
|
+
listView.setScope('all')
|
|
315
446
|
},
|
|
316
447
|
}
|
|
317
448
|
: undefined
|
|
318
449
|
}
|
|
319
450
|
sort={sort}
|
|
320
|
-
onSortChange={
|
|
451
|
+
onSortChange={(next) => {
|
|
452
|
+
listView.setSort(next)
|
|
453
|
+
}}
|
|
454
|
+
visibleColumnKeys={listView.visibleKeys}
|
|
321
455
|
/>
|
|
322
456
|
)}
|
|
323
457
|
{updatePartnership.error !== null && (
|
|
@@ -325,17 +459,8 @@ export function PartnershipsPage(): React.JSX.Element {
|
|
|
325
459
|
<ErrorPanel error={updatePartnership.error} />
|
|
326
460
|
</div>
|
|
327
461
|
)}
|
|
328
|
-
{
|
|
329
|
-
|
|
330
|
-
type="button"
|
|
331
|
-
onClick={partnerships.loadMore}
|
|
332
|
-
disabled={partnerships.isLoadingMore}
|
|
333
|
-
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"
|
|
334
|
-
>
|
|
335
|
-
{partnerships.isLoadingMore ? 'Loading…' : 'Load more'}
|
|
336
|
-
</button>
|
|
337
|
-
)}
|
|
338
|
-
{companies.hasMore && (
|
|
462
|
+
{view === 'list' && <Paginator list={partnerships} />}
|
|
463
|
+
{companies.hasNext && (
|
|
339
464
|
<p className="mt-2 text-[11px] text-ink-faint">
|
|
340
465
|
More companies exist than one page returns, so some company names may show as “—”.
|
|
341
466
|
</p>
|
package/src/pages/PeoplePage.tsx
CHANGED
|
@@ -2,14 +2,33 @@ import type { Person } from '@kelpie/schemas'
|
|
|
2
2
|
import { useState } from 'react'
|
|
3
3
|
import { useNavigate } from 'react-router'
|
|
4
4
|
|
|
5
|
+
import { useTimezone } from '../api/resources/account.ts'
|
|
5
6
|
import { useCreatePerson, usePeople } from '../api/resources/people.ts'
|
|
6
7
|
import { Chip } from '../components/Chip.tsx'
|
|
8
|
+
import { ColumnPicker } from '../components/ColumnPicker.tsx'
|
|
7
9
|
import { DataTable } from '../components/DataTable.tsx'
|
|
8
10
|
import type { Column } from '../components/DataTable.tsx'
|
|
9
11
|
import { FilterBar, PageHeader } from '../components/PageHeader.tsx'
|
|
12
|
+
import { Paginator } from '../components/Paginator.tsx'
|
|
10
13
|
import { ErrorPanel, LoadingPanel } from '../components/QueryState.tsx'
|
|
14
|
+
import { formatDate } from '../lib/dates.ts'
|
|
15
|
+
import { useListView } from '../lib/listView.ts'
|
|
16
|
+
import { serverSortOnly } from '../lib/sort.ts'
|
|
11
17
|
import { usePeopleDirectory } from './positionDirectory.ts'
|
|
12
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Column catalog for People. Every field the wire schema carries appears here,
|
|
21
|
+
* plus the two joined-in columns (position title, company names) computed from
|
|
22
|
+
* the position directory. `sortKey` names the server field; `getSortValue`
|
|
23
|
+
* enables the DataTable's page-local sort for columns the server does not
|
|
24
|
+
* index. Everything not in `DEFAULT_VISIBLE_KEYS` starts hidden and comes back
|
|
25
|
+
* through the Columns picker.
|
|
26
|
+
*/
|
|
27
|
+
const DEFAULT_VISIBLE_KEYS: readonly string[] = ['name', 'email', 'phones', 'company', 'updatedAt']
|
|
28
|
+
|
|
29
|
+
/** The sort fields `/v1/people` accepts. Anything else is a client-only sort. */
|
|
30
|
+
const SERVER_SORT_KEYS: readonly string[] = ['name', 'created_at', 'updated_at']
|
|
31
|
+
|
|
13
32
|
/**
|
|
14
33
|
* The People list.
|
|
15
34
|
*
|
|
@@ -22,8 +41,12 @@ export function PeoplePage(): React.JSX.Element {
|
|
|
22
41
|
const [term, setTerm] = useState('')
|
|
23
42
|
const [sort, setSort] = useState<string | undefined>(undefined)
|
|
24
43
|
const navigate = useNavigate()
|
|
44
|
+
const timezone = useTimezone()
|
|
25
45
|
const hasFilter = term.trim().length > 0
|
|
26
|
-
const people = usePeople({
|
|
46
|
+
const people = usePeople({
|
|
47
|
+
term: hasFilter ? term.trim() : undefined,
|
|
48
|
+
sort: serverSortOnly(sort, SERVER_SORT_KEYS),
|
|
49
|
+
})
|
|
27
50
|
const directory = usePeopleDirectory(people.records.map((person) => person.id))
|
|
28
51
|
const createPerson = useCreatePerson()
|
|
29
52
|
|
|
@@ -45,20 +68,100 @@ export function PeoplePage(): React.JSX.Element {
|
|
|
45
68
|
{
|
|
46
69
|
key: 'position',
|
|
47
70
|
header: 'Position',
|
|
71
|
+
getSortValue: (person) => directory.titleFor(person.id) ?? null,
|
|
48
72
|
render: (person) => directory.titleFor(person.id) ?? '—',
|
|
49
73
|
},
|
|
50
74
|
{
|
|
51
75
|
key: 'company',
|
|
52
76
|
header: 'Company',
|
|
77
|
+
getSortValue: (person) => directory.companyNamesFor(person.id).join(', ') || null,
|
|
53
78
|
render: (person) => {
|
|
54
79
|
const names = directory.companyNamesFor(person.id)
|
|
55
80
|
|
|
56
81
|
return names.length > 0 ? names.join(', ') : '—'
|
|
57
82
|
},
|
|
58
83
|
},
|
|
84
|
+
{
|
|
85
|
+
key: 'email',
|
|
86
|
+
header: 'Email',
|
|
87
|
+
getSortValue: (person) => person.email,
|
|
88
|
+
render: (person) =>
|
|
89
|
+
person.email === null ? (
|
|
90
|
+
'—'
|
|
91
|
+
) : (
|
|
92
|
+
<span className="font-mono text-[12px] text-ink-muted">{person.email}</span>
|
|
93
|
+
),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
key: 'phones',
|
|
97
|
+
header: 'Phones',
|
|
98
|
+
getSortValue: (person) => person.phones.join(', ') || null,
|
|
99
|
+
render: (person) =>
|
|
100
|
+
person.phones.length === 0 ? '—' : (
|
|
101
|
+
<span className="font-mono text-[12px] text-ink-muted">{person.phones.join(', ')}</span>
|
|
102
|
+
),
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
key: 'socials',
|
|
106
|
+
header: 'Socials',
|
|
107
|
+
getSortValue: (person) => person.socialProfiles.map((entry) => entry.network).join(', ') || null,
|
|
108
|
+
render: (person) =>
|
|
109
|
+
person.socialProfiles.length === 0 ? '—' : (
|
|
110
|
+
<span className="flex flex-wrap gap-1">
|
|
111
|
+
{person.socialProfiles.map((entry) => (
|
|
112
|
+
<Chip key={entry.network}>
|
|
113
|
+
<span className="text-[10px]">{entry.network}</span>
|
|
114
|
+
</Chip>
|
|
115
|
+
))}
|
|
116
|
+
</span>
|
|
117
|
+
),
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
key: 'location',
|
|
121
|
+
header: 'Location',
|
|
122
|
+
getSortValue: (person) => person.location,
|
|
123
|
+
render: (person) => person.location ?? '—',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
key: 'timezone',
|
|
127
|
+
header: 'Timezone',
|
|
128
|
+
getSortValue: (person) => person.timezone,
|
|
129
|
+
render: (person) =>
|
|
130
|
+
person.timezone === null ? '—' : (
|
|
131
|
+
<span className="font-mono text-[12px] text-ink-muted">{person.timezone}</span>
|
|
132
|
+
),
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
key: 'preferredChannel',
|
|
136
|
+
header: 'Preferred channel',
|
|
137
|
+
getSortValue: (person) => person.preferredChannel,
|
|
138
|
+
render: (person) => <span className="capitalize">{person.preferredChannel}</span>,
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
key: 'influence',
|
|
142
|
+
header: 'Influence',
|
|
143
|
+
getSortValue: (person) => person.influence,
|
|
144
|
+
render: (person) => <span className="capitalize">{person.influence.replace('_', ' ')}</span>,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
key: 'relationship',
|
|
148
|
+
header: 'Relationship',
|
|
149
|
+
getSortValue: (person) => person.relationship,
|
|
150
|
+
render: (person) => <span className="capitalize">{person.relationship}</span>,
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
key: 'summary',
|
|
154
|
+
header: 'Summary',
|
|
155
|
+
getSortValue: (person) => person.summary || null,
|
|
156
|
+
render: (person) =>
|
|
157
|
+
person.summary.length === 0 ? '—' : (
|
|
158
|
+
<span className="text-ink-muted">{person.summary}</span>
|
|
159
|
+
),
|
|
160
|
+
},
|
|
59
161
|
{
|
|
60
162
|
key: 'tags',
|
|
61
163
|
header: 'Tags',
|
|
164
|
+
getSortValue: (person) => person.tags.join(', ') || null,
|
|
62
165
|
render: (person) =>
|
|
63
166
|
person.tags.length > 0 ? (
|
|
64
167
|
<span className="flex flex-wrap gap-1">
|
|
@@ -72,8 +175,31 @@ export function PeoplePage(): React.JSX.Element {
|
|
|
72
175
|
'—'
|
|
73
176
|
),
|
|
74
177
|
},
|
|
178
|
+
{
|
|
179
|
+
key: 'lastContactedAt',
|
|
180
|
+
header: 'Last contacted',
|
|
181
|
+
getSortValue: (person) => person.lastContactedAt,
|
|
182
|
+
render: (person) =>
|
|
183
|
+
person.lastContactedAt === null ? '—' : formatDate(person.lastContactedAt, timezone),
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
key: 'createdAt',
|
|
187
|
+
header: 'Created',
|
|
188
|
+
sortKey: 'created_at',
|
|
189
|
+
render: (person) => formatDate(person.createdAt, timezone),
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
key: 'updatedAt',
|
|
193
|
+
header: 'Updated',
|
|
194
|
+
sortKey: 'updated_at',
|
|
195
|
+
render: (person) => formatDate(person.updatedAt, timezone),
|
|
196
|
+
},
|
|
75
197
|
]
|
|
76
198
|
|
|
199
|
+
const supportedKeys = columns.map((column) => column.key)
|
|
200
|
+
const listView = useListView('people', supportedKeys, DEFAULT_VISIBLE_KEYS)
|
|
201
|
+
const pickerOptions = columns.map((column) => ({ key: column.key, label: column.header }))
|
|
202
|
+
|
|
77
203
|
return (
|
|
78
204
|
<div className="animate-fade-in">
|
|
79
205
|
<PageHeader
|
|
@@ -82,6 +208,13 @@ export function PeoplePage(): React.JSX.Element {
|
|
|
82
208
|
void addPerson()
|
|
83
209
|
}}
|
|
84
210
|
addLabel="Add person"
|
|
211
|
+
actions={
|
|
212
|
+
<ColumnPicker
|
|
213
|
+
options={pickerOptions}
|
|
214
|
+
visibleKeys={listView.visibleKeys}
|
|
215
|
+
onChange={listView.setVisibleKeys}
|
|
216
|
+
/>
|
|
217
|
+
}
|
|
85
218
|
/>
|
|
86
219
|
<FilterBar
|
|
87
220
|
value={term}
|
|
@@ -101,6 +234,7 @@ export function PeoplePage(): React.JSX.Element {
|
|
|
101
234
|
<LoadingPanel label="Loading people…" />
|
|
102
235
|
) : (
|
|
103
236
|
<>
|
|
237
|
+
<Paginator list={people} placement="top" />
|
|
104
238
|
<DataTable
|
|
105
239
|
columns={columns}
|
|
106
240
|
rows={people.records}
|
|
@@ -122,22 +256,14 @@ export function PeoplePage(): React.JSX.Element {
|
|
|
122
256
|
}
|
|
123
257
|
sort={sort}
|
|
124
258
|
onSortChange={setSort}
|
|
259
|
+
visibleColumnKeys={listView.visibleKeys}
|
|
125
260
|
/>
|
|
126
261
|
{!directory.isComplete && !directory.isLoading && (
|
|
127
262
|
<p className="mt-2 text-[11px] text-ink-faint">
|
|
128
263
|
These people hold more positions than one page returns, so some rows may read “—”.
|
|
129
264
|
</p>
|
|
130
265
|
)}
|
|
131
|
-
{people
|
|
132
|
-
<button
|
|
133
|
-
type="button"
|
|
134
|
-
onClick={people.loadMore}
|
|
135
|
-
disabled={people.isLoadingMore}
|
|
136
|
-
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"
|
|
137
|
-
>
|
|
138
|
-
{people.isLoadingMore ? 'Loading…' : 'Load more'}
|
|
139
|
-
</button>
|
|
140
|
-
)}
|
|
266
|
+
<Paginator list={people} />
|
|
141
267
|
</>
|
|
142
268
|
)}
|
|
143
269
|
</div>
|
|
@@ -27,6 +27,7 @@ import { InlineEdit } from '../components/InlineEdit.tsx'
|
|
|
27
27
|
import { NotesPanel } from '../components/NotesPanel.tsx'
|
|
28
28
|
import { RelatedPlanAttention } from '../components/PlanAttention.tsx'
|
|
29
29
|
import { ErrorPanel, LoadingPanel, NotFoundPanel } from '../components/QueryState.tsx'
|
|
30
|
+
import { ListsPanel } from '../components/ListsPanel.tsx'
|
|
30
31
|
import { RecordTabs } from '../components/RecordTabs.tsx'
|
|
31
32
|
import type { RecordTabDescriptor } from '../components/RecordTabs.tsx'
|
|
32
33
|
import { SectionHeader } from '../components/SectionHeader.tsx'
|
|
@@ -94,6 +95,7 @@ export function PersonDetail(): React.JSX.Element {
|
|
|
94
95
|
: [{ id: 'hiring', label: 'Hiring', count: candidacies.records.length }]),
|
|
95
96
|
{ id: 'notes', label: 'Notes' },
|
|
96
97
|
{ id: 'decisions', label: 'Decisions' },
|
|
98
|
+
{ id: 'lists', label: 'Lists' },
|
|
97
99
|
...moduleTabs.map((tab) => ({ id: tab.id, label: tab.label })),
|
|
98
100
|
]
|
|
99
101
|
const active = tabs.some((tab) => tab.id === activeTab) ? activeTab : 'overview'
|
|
@@ -136,6 +138,7 @@ export function PersonDetail(): React.JSX.Element {
|
|
|
136
138
|
)}
|
|
137
139
|
{active === 'notes' && <NotesPanel targetType="person" targetId={record.id} />}
|
|
138
140
|
{active === 'decisions' && <DecisionsPanel targetType="person" targetId={record.id} />}
|
|
141
|
+
{active === 'lists' && <ListsPanel targetType="person" targetId={record.id} />}
|
|
139
142
|
{moduleTab?.render({ objectType: 'person', recordId: record.id })}
|
|
140
143
|
</RecordTabs>
|
|
141
144
|
</div>
|
|
@@ -408,7 +411,7 @@ function PersonPositions({ person }: { readonly person: Person }): React.JSX.Ele
|
|
|
408
411
|
function submit(event: FormEvent): void {
|
|
409
412
|
event.preventDefault()
|
|
410
413
|
|
|
411
|
-
if (companyId.length === 0
|
|
414
|
+
if (companyId.length === 0) {
|
|
412
415
|
return
|
|
413
416
|
}
|
|
414
417
|
|
|
@@ -500,9 +503,8 @@ function PersonPositions({ person }: { readonly person: Person }): React.JSX.Ele
|
|
|
500
503
|
onChange={(event) => {
|
|
501
504
|
setTitle(event.target.value)
|
|
502
505
|
}}
|
|
503
|
-
placeholder="Position title"
|
|
506
|
+
placeholder="Position title (optional)"
|
|
504
507
|
className="w-full rounded-md border border-border bg-surface-raised px-2 py-1.5 text-[12px] outline-none focus:border-accent"
|
|
505
|
-
required
|
|
506
508
|
/>
|
|
507
509
|
<div className="flex justify-end gap-2">
|
|
508
510
|
<button
|