@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
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
MATCH_KEYS,
|
|
6
6
|
OBJECT_COLUMNS,
|
|
7
7
|
OBJECT_LABELS,
|
|
8
|
+
ON_MISSING_COMPANY_LABELS,
|
|
8
9
|
SOURCE_LABELS,
|
|
9
10
|
defaultMatchKeyId,
|
|
10
11
|
findMatchKey,
|
|
@@ -18,11 +19,15 @@ import type {
|
|
|
18
19
|
ImportObject,
|
|
19
20
|
ImportPreviewRow,
|
|
20
21
|
ImportRowAction,
|
|
22
|
+
ImportRowError,
|
|
21
23
|
ImportSource,
|
|
24
|
+
OnMissingCompany,
|
|
25
|
+
SampleDataCounts,
|
|
22
26
|
} from '@kelpie/schemas'
|
|
23
|
-
import { useState } from 'react'
|
|
24
|
-
import type { ChangeEvent, FormEvent, ReactNode } from 'react'
|
|
27
|
+
import { useEffect, useState } from 'react'
|
|
28
|
+
import type { ChangeEvent, DragEvent, FormEvent, ReactNode } from 'react'
|
|
25
29
|
|
|
30
|
+
import { useRelinkEmailDomains } from '../../api/resources/emailDomainLinker.ts'
|
|
26
31
|
import {
|
|
27
32
|
saveCsv,
|
|
28
33
|
useCommitImportJob,
|
|
@@ -31,6 +36,8 @@ import {
|
|
|
31
36
|
useExportCsv,
|
|
32
37
|
useImportJob,
|
|
33
38
|
} from '../../api/resources/importJobs.ts'
|
|
39
|
+
import { useInstallSampleData } from '../../api/resources/sampleData.ts'
|
|
40
|
+
import { useSession } from '../../api/resources/session.ts'
|
|
34
41
|
import { PageHeader } from '../../components/PageHeader.tsx'
|
|
35
42
|
|
|
36
43
|
/**
|
|
@@ -46,13 +53,14 @@ import { PageHeader } from '../../components/PageHeader.tsx'
|
|
|
46
53
|
* `public/fixtures`, and none of those are part of the application.
|
|
47
54
|
*/
|
|
48
55
|
|
|
49
|
-
type WizardStep = 'source' | 'upload' | 'map' | 'result'
|
|
56
|
+
type WizardStep = 'source' | 'upload' | 'map' | 'result' | 'done'
|
|
50
57
|
|
|
51
58
|
const STEPS: readonly { readonly id: WizardStep; readonly label: string }[] = [
|
|
52
59
|
{ id: 'source', label: 'Source' },
|
|
53
60
|
{ id: 'upload', label: 'Upload' },
|
|
54
61
|
{ id: 'map', label: 'Map' },
|
|
55
62
|
{ id: 'result', label: 'Dry-run' },
|
|
63
|
+
{ id: 'done', label: 'Done' },
|
|
56
64
|
]
|
|
57
65
|
|
|
58
66
|
export function DataPage(): ReactNode {
|
|
@@ -64,6 +72,7 @@ export function DataPage(): ReactNode {
|
|
|
64
72
|
const [file, setFile] = useState<File | null>(null)
|
|
65
73
|
const [columnMap, setColumnMap] = useState<ImportColumnMap>({})
|
|
66
74
|
const [conflictMode, setConflictMode] = useState<ImportConflictMode>('skip')
|
|
75
|
+
const [onMissingCompany, setOnMissingCompany] = useState<OnMissingCompany>('skip')
|
|
67
76
|
const [matchKeyId, setMatchKeyId] = useState<string>(() => defaultMatchKeyId('companies'))
|
|
68
77
|
const [jobId, setJobId] = useState<string | undefined>(undefined)
|
|
69
78
|
const [problem, setProblem] = useState<string | null>(null)
|
|
@@ -115,6 +124,7 @@ export function DataPage(): ReactNode {
|
|
|
115
124
|
setFile(null)
|
|
116
125
|
setColumnMap({})
|
|
117
126
|
setConflictMode('skip')
|
|
127
|
+
setOnMissingCompany('skip')
|
|
118
128
|
setMatchKeyId(defaultMatchKeyId(object))
|
|
119
129
|
setJobId(undefined)
|
|
120
130
|
setProblem(null)
|
|
@@ -133,6 +143,7 @@ export function DataPage(): ReactNode {
|
|
|
133
143
|
source,
|
|
134
144
|
object,
|
|
135
145
|
conflictMode,
|
|
146
|
+
onMissingCompany,
|
|
136
147
|
matchKeyId,
|
|
137
148
|
...(map === undefined ? {} : { columnMap: map }),
|
|
138
149
|
})
|
|
@@ -147,6 +158,19 @@ export function DataPage(): ReactNode {
|
|
|
147
158
|
return created
|
|
148
159
|
}
|
|
149
160
|
|
|
161
|
+
async function processChosenFile(chosen: File): Promise<void> {
|
|
162
|
+
setProblem(null)
|
|
163
|
+
setFile(chosen)
|
|
164
|
+
|
|
165
|
+
const created = await runDryRun(chosen, undefined)
|
|
166
|
+
|
|
167
|
+
// The first upload takes the server's own reading of the file, which is what
|
|
168
|
+
// the mapping table is then built from.
|
|
169
|
+
setColumnMap(created.columnMap)
|
|
170
|
+
setMatchKeyId(created.matchKey)
|
|
171
|
+
setStep('map')
|
|
172
|
+
}
|
|
173
|
+
|
|
150
174
|
async function onFileChange(event: ChangeEvent<HTMLInputElement>): Promise<void> {
|
|
151
175
|
const chosen = event.target.files?.[0]
|
|
152
176
|
|
|
@@ -158,16 +182,22 @@ export function DataPage(): ReactNode {
|
|
|
158
182
|
return
|
|
159
183
|
}
|
|
160
184
|
|
|
161
|
-
|
|
162
|
-
|
|
185
|
+
await processChosenFile(chosen)
|
|
186
|
+
}
|
|
163
187
|
|
|
164
|
-
|
|
188
|
+
async function onFileDrop(event: DragEvent<HTMLLabelElement>): Promise<void> {
|
|
189
|
+
event.preventDefault()
|
|
165
190
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
191
|
+
if (createJob.isPending) {
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const chosen = event.dataTransfer.files[0]
|
|
196
|
+
if (chosen === undefined) {
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
await processChosenFile(chosen)
|
|
171
201
|
}
|
|
172
202
|
|
|
173
203
|
async function onRerun(event: FormEvent): Promise<void> {
|
|
@@ -193,6 +223,19 @@ export function DataPage(): ReactNode {
|
|
|
193
223
|
setStep('result')
|
|
194
224
|
}
|
|
195
225
|
|
|
226
|
+
// Commit finishes on the server, not the click, so the wizard waits for the
|
|
227
|
+
// job to reach `completed` before showing the outcome step. `failed` lands
|
|
228
|
+
// here too — the outcome is that the commit did not go through.
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
if (step !== 'result' || job === undefined) {
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (job.status === 'completed' || job.status === 'failed') {
|
|
235
|
+
setStep('done')
|
|
236
|
+
}
|
|
237
|
+
}, [step, job])
|
|
238
|
+
|
|
196
239
|
function onCommit(): void {
|
|
197
240
|
// The file goes back with the commit: the server kept its digest, not its
|
|
198
241
|
// bytes. This is the same `File` every dry run was run against.
|
|
@@ -217,7 +260,11 @@ export function DataPage(): ReactNode {
|
|
|
217
260
|
</p>
|
|
218
261
|
)}
|
|
219
262
|
|
|
220
|
-
<
|
|
263
|
+
<SampleDataSection />
|
|
264
|
+
|
|
265
|
+
<RelinkEmailDomainsSection />
|
|
266
|
+
|
|
267
|
+
<section className="space-y-4 border-t border-border pt-8">
|
|
221
268
|
<div>
|
|
222
269
|
<h2 className="text-[15px] font-semibold text-ink">Export</h2>
|
|
223
270
|
<p className="mt-1 text-[13px] text-ink-muted">
|
|
@@ -342,7 +389,14 @@ export function DataPage(): ReactNode {
|
|
|
342
389
|
<p className="text-[13px] text-ink-muted">
|
|
343
390
|
{SOURCE_LABELS[source]} → {OBJECT_LABELS[object]}
|
|
344
391
|
</p>
|
|
345
|
-
<label
|
|
392
|
+
<label
|
|
393
|
+
className="flex cursor-pointer flex-col items-center justify-center rounded-lg border border-dashed border-border bg-surface-raised px-6 py-10 transition hover:border-accent"
|
|
394
|
+
onDragOver={(event) => {
|
|
395
|
+
event.preventDefault()
|
|
396
|
+
event.dataTransfer.dropEffect = 'copy'
|
|
397
|
+
}}
|
|
398
|
+
onDrop={(event) => runStep(onFileDrop(event))}
|
|
399
|
+
>
|
|
346
400
|
<span className="text-[13px] font-medium text-ink">
|
|
347
401
|
{createJob.isPending ? 'Reading the file…' : 'Drop a CSV here or click to browse'}
|
|
348
402
|
</span>
|
|
@@ -373,10 +427,12 @@ export function DataPage(): ReactNode {
|
|
|
373
427
|
object={object}
|
|
374
428
|
columnMap={columnMap}
|
|
375
429
|
conflictMode={conflictMode}
|
|
430
|
+
onMissingCompany={onMissingCompany}
|
|
376
431
|
matchKeyId={matchKeyId}
|
|
377
432
|
isPending={createJob.isPending}
|
|
378
433
|
onColumnMapChange={setColumnMap}
|
|
379
434
|
onConflictModeChange={setConflictMode}
|
|
435
|
+
onMissingCompanyChange={setOnMissingCompany}
|
|
380
436
|
onMatchKeyChange={setMatchKeyId}
|
|
381
437
|
onSubmit={(event) => runStep(onRerun(event))}
|
|
382
438
|
onBack={() => {
|
|
@@ -397,6 +453,10 @@ export function DataPage(): ReactNode {
|
|
|
397
453
|
onBack={() => setStep('map')}
|
|
398
454
|
/>
|
|
399
455
|
) : null}
|
|
456
|
+
|
|
457
|
+
{step === 'done' && job !== undefined ? (
|
|
458
|
+
<DonePanel job={job} object={object} onAgain={resetWizard} />
|
|
459
|
+
) : null}
|
|
400
460
|
</section>
|
|
401
461
|
</div>
|
|
402
462
|
)
|
|
@@ -407,10 +467,12 @@ interface MappingFormProps {
|
|
|
407
467
|
readonly object: ImportObject
|
|
408
468
|
readonly columnMap: ImportColumnMap
|
|
409
469
|
readonly conflictMode: ImportConflictMode
|
|
470
|
+
readonly onMissingCompany: OnMissingCompany
|
|
410
471
|
readonly matchKeyId: string
|
|
411
472
|
readonly isPending: boolean
|
|
412
473
|
readonly onColumnMapChange: (map: ImportColumnMap) => void
|
|
413
474
|
readonly onConflictModeChange: (mode: ImportConflictMode) => void
|
|
475
|
+
readonly onMissingCompanyChange: (mode: OnMissingCompany) => void
|
|
414
476
|
readonly onMatchKeyChange: (id: string) => void
|
|
415
477
|
readonly onSubmit: (event: FormEvent) => void
|
|
416
478
|
readonly onBack: () => void
|
|
@@ -419,6 +481,9 @@ interface MappingFormProps {
|
|
|
419
481
|
function MappingForm(props: MappingFormProps): ReactNode {
|
|
420
482
|
const matchKey = findMatchKey(props.object, props.matchKeyId)
|
|
421
483
|
const keyColumns = new Set(matchKey?.columns ?? [])
|
|
484
|
+
// People rows can carry a company and title that drive a Position, so only a
|
|
485
|
+
// People import has a company to be missing.
|
|
486
|
+
const showMissingCompany = props.object === 'people'
|
|
422
487
|
|
|
423
488
|
return (
|
|
424
489
|
<form className="space-y-4" onSubmit={props.onSubmit}>
|
|
@@ -453,6 +518,23 @@ function MappingForm(props: MappingFormProps): ReactNode {
|
|
|
453
518
|
))}
|
|
454
519
|
</select>
|
|
455
520
|
</Field>
|
|
521
|
+
{showMissingCompany ? (
|
|
522
|
+
<Field label="Missing company">
|
|
523
|
+
<select
|
|
524
|
+
value={props.onMissingCompany}
|
|
525
|
+
onChange={(event) =>
|
|
526
|
+
props.onMissingCompanyChange(event.target.value as OnMissingCompany)
|
|
527
|
+
}
|
|
528
|
+
className="w-full rounded-md border border-border bg-surface-raised px-3 py-2 text-[13px] outline-none focus:border-accent"
|
|
529
|
+
>
|
|
530
|
+
{Object.entries(ON_MISSING_COMPANY_LABELS).map(([mode, label]) => (
|
|
531
|
+
<option key={mode} value={mode}>
|
|
532
|
+
{label}
|
|
533
|
+
</option>
|
|
534
|
+
))}
|
|
535
|
+
</select>
|
|
536
|
+
</Field>
|
|
537
|
+
) : null}
|
|
456
538
|
</div>
|
|
457
539
|
<p className="text-[12px] text-ink-muted">
|
|
458
540
|
{props.conflictMode === 'update'
|
|
@@ -460,6 +542,16 @@ function MappingForm(props: MappingFormProps): ReactNode {
|
|
|
460
542
|
: 'Rows matching an existing record on the key column are skipped.'}{' '}
|
|
461
543
|
Key columns must be mapped.
|
|
462
544
|
</p>
|
|
545
|
+
{showMissingCompany ? (
|
|
546
|
+
<p className="text-[12px] text-ink-muted">
|
|
547
|
+
Map <span className="font-mono text-[11px]">company_domain</span> or{' '}
|
|
548
|
+
<span className="font-mono text-[11px]">company_name</span> and{' '}
|
|
549
|
+
<span className="font-mono text-[11px]">title</span> to link a position for each person.{' '}
|
|
550
|
+
{props.onMissingCompany === 'create'
|
|
551
|
+
? 'A company that is not here yet is created from the row.'
|
|
552
|
+
: 'A company that is not here yet is left unlinked and reported as a warning.'}
|
|
553
|
+
</p>
|
|
554
|
+
) : null}
|
|
463
555
|
<div className="overflow-hidden rounded-md border border-border">
|
|
464
556
|
<table className="w-full text-left text-[13px]">
|
|
465
557
|
<thead>
|
|
@@ -576,29 +668,7 @@ function ResultPanel({ job, object, isCommitting, onCommit, onBack }: ResultPane
|
|
|
576
668
|
|
|
577
669
|
{job.errors.length > 0 ? (
|
|
578
670
|
<div className="space-y-2">
|
|
579
|
-
<
|
|
580
|
-
<table className="w-full text-left text-[13px]">
|
|
581
|
-
<thead>
|
|
582
|
-
<tr className="border-b border-border bg-surface text-[11px] font-semibold tracking-wide text-ink-muted uppercase">
|
|
583
|
-
<th className="px-4 py-2.5">Row</th>
|
|
584
|
-
<th className="px-4 py-2.5">Field</th>
|
|
585
|
-
<th className="px-4 py-2.5">Message</th>
|
|
586
|
-
</tr>
|
|
587
|
-
</thead>
|
|
588
|
-
<tbody>
|
|
589
|
-
{job.errors.map((error, index) => (
|
|
590
|
-
<tr
|
|
591
|
-
key={`${String(error.row)}-${error.field}-${String(index)}`}
|
|
592
|
-
className="border-b border-border last:border-0"
|
|
593
|
-
>
|
|
594
|
-
<td className="px-4 py-2.5 font-mono text-[12px]">{error.row}</td>
|
|
595
|
-
<td className="px-4 py-2.5 font-mono text-[12px]">{error.field}</td>
|
|
596
|
-
<td className="px-4 py-2.5 text-ink-muted">{error.message}</td>
|
|
597
|
-
</tr>
|
|
598
|
-
))}
|
|
599
|
-
</tbody>
|
|
600
|
-
</table>
|
|
601
|
-
</div>
|
|
671
|
+
<IssueTable rows={job.errors} />
|
|
602
672
|
{job.counts.error > job.errors.length ? (
|
|
603
673
|
<p className="text-[12px] text-ink-muted">
|
|
604
674
|
Showing {job.errors.length} of {job.counts.error} failing rows.
|
|
@@ -609,6 +679,19 @@ function ResultPanel({ job, object, isCommitting, onCommit, onBack }: ResultPane
|
|
|
609
679
|
<p className="text-[13px] text-ink-muted">No row errors.</p>
|
|
610
680
|
)}
|
|
611
681
|
|
|
682
|
+
{job.warnings.length > 0 ? (
|
|
683
|
+
<div className="space-y-2">
|
|
684
|
+
<div>
|
|
685
|
+
<h3 className="text-[13px] font-semibold text-ink">Warnings</h3>
|
|
686
|
+
<p className="mt-0.5 text-[12px] text-ink-muted">
|
|
687
|
+
These rows were imported. Each note says what was left out, such as a person whose
|
|
688
|
+
company was not here to link a position to.
|
|
689
|
+
</p>
|
|
690
|
+
</div>
|
|
691
|
+
<IssueTable rows={job.warnings} />
|
|
692
|
+
</div>
|
|
693
|
+
) : null}
|
|
694
|
+
|
|
612
695
|
<div className="flex flex-wrap gap-3">
|
|
613
696
|
<button
|
|
614
697
|
type="button"
|
|
@@ -630,6 +713,110 @@ function ResultPanel({ job, object, isCommitting, onCommit, onBack }: ResultPane
|
|
|
630
713
|
)
|
|
631
714
|
}
|
|
632
715
|
|
|
716
|
+
interface DonePanelProps {
|
|
717
|
+
readonly job: ImportJob
|
|
718
|
+
readonly object: ImportObject
|
|
719
|
+
readonly onAgain: () => void
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function DonePanel({ job, object, onAgain }: DonePanelProps): ReactNode {
|
|
723
|
+
const failed = job.status === 'failed'
|
|
724
|
+
const wrote = job.counts.create + job.counts.update
|
|
725
|
+
|
|
726
|
+
return (
|
|
727
|
+
<div className="space-y-4">
|
|
728
|
+
<div>
|
|
729
|
+
<h3 className="text-[15px] font-semibold text-ink">
|
|
730
|
+
{failed ? 'Import failed' : 'Import complete'}
|
|
731
|
+
</h3>
|
|
732
|
+
<p className="mt-1 text-[13px] text-ink-muted">
|
|
733
|
+
{failed
|
|
734
|
+
? 'The server refused the commit. Nothing was written.'
|
|
735
|
+
: `${wrote} ${OBJECT_LABELS[object].toLowerCase()} row${wrote === 1 ? '' : 's'} written to this workspace.`}
|
|
736
|
+
</p>
|
|
737
|
+
</div>
|
|
738
|
+
<p className="text-[12px] text-ink-muted">
|
|
739
|
+
Job <span className="font-mono text-[12px] text-ink">{job.id}</span> · status {job.status}
|
|
740
|
+
</p>
|
|
741
|
+
<div className="grid grid-cols-2 gap-3 sm:grid-cols-5">
|
|
742
|
+
<Stat label="Total" value={job.counts.total} />
|
|
743
|
+
<Stat label="Created" value={job.counts.create} />
|
|
744
|
+
<Stat label="Updated" value={job.counts.update} />
|
|
745
|
+
<Stat label="Skipped" value={job.counts.skip} />
|
|
746
|
+
<Stat label="Errors" value={job.counts.error} danger />
|
|
747
|
+
</div>
|
|
748
|
+
|
|
749
|
+
{job.errors.length > 0 ? (
|
|
750
|
+
<div className="space-y-2">
|
|
751
|
+
<div>
|
|
752
|
+
<h3 className="text-[13px] font-semibold text-ink">Row errors</h3>
|
|
753
|
+
<p className="mt-0.5 text-[12px] text-ink-muted">
|
|
754
|
+
These rows were not written. Fix them in the CSV and import again.
|
|
755
|
+
</p>
|
|
756
|
+
</div>
|
|
757
|
+
<IssueTable rows={job.errors} />
|
|
758
|
+
{job.counts.error > job.errors.length ? (
|
|
759
|
+
<p className="text-[12px] text-ink-muted">
|
|
760
|
+
Showing {job.errors.length} of {job.counts.error} failing rows.
|
|
761
|
+
</p>
|
|
762
|
+
) : null}
|
|
763
|
+
</div>
|
|
764
|
+
) : null}
|
|
765
|
+
|
|
766
|
+
{job.warnings.length > 0 ? (
|
|
767
|
+
<div className="space-y-2">
|
|
768
|
+
<div>
|
|
769
|
+
<h3 className="text-[13px] font-semibold text-ink">Warnings</h3>
|
|
770
|
+
<p className="mt-0.5 text-[12px] text-ink-muted">
|
|
771
|
+
These rows were written. Each note says what was left out.
|
|
772
|
+
</p>
|
|
773
|
+
</div>
|
|
774
|
+
<IssueTable rows={job.warnings} />
|
|
775
|
+
</div>
|
|
776
|
+
) : null}
|
|
777
|
+
|
|
778
|
+
<div className="flex flex-wrap gap-3">
|
|
779
|
+
<button
|
|
780
|
+
type="button"
|
|
781
|
+
onClick={onAgain}
|
|
782
|
+
className="rounded-md bg-accent px-3.5 py-2 text-[12px] font-semibold text-accent-fg transition hover:bg-accent-hover"
|
|
783
|
+
>
|
|
784
|
+
Import another
|
|
785
|
+
</button>
|
|
786
|
+
</div>
|
|
787
|
+
</div>
|
|
788
|
+
)
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/** The row / field / message table shared by the error and warning lists. */
|
|
792
|
+
function IssueTable({ rows }: { readonly rows: readonly ImportRowError[] }): ReactNode {
|
|
793
|
+
return (
|
|
794
|
+
<div className="overflow-hidden rounded-md border border-border">
|
|
795
|
+
<table className="w-full text-left text-[13px]">
|
|
796
|
+
<thead>
|
|
797
|
+
<tr className="border-b border-border bg-surface text-[11px] font-semibold tracking-wide text-ink-muted uppercase">
|
|
798
|
+
<th className="px-4 py-2.5">Row</th>
|
|
799
|
+
<th className="px-4 py-2.5">Field</th>
|
|
800
|
+
<th className="px-4 py-2.5">Message</th>
|
|
801
|
+
</tr>
|
|
802
|
+
</thead>
|
|
803
|
+
<tbody>
|
|
804
|
+
{rows.map((issue, index) => (
|
|
805
|
+
<tr
|
|
806
|
+
key={`${String(issue.row)}-${issue.field}-${String(index)}`}
|
|
807
|
+
className="border-b border-border last:border-0"
|
|
808
|
+
>
|
|
809
|
+
<td className="px-4 py-2.5 font-mono text-[12px]">{issue.row}</td>
|
|
810
|
+
<td className="px-4 py-2.5 font-mono text-[12px]">{issue.field}</td>
|
|
811
|
+
<td className="px-4 py-2.5 text-ink-muted">{issue.message}</td>
|
|
812
|
+
</tr>
|
|
813
|
+
))}
|
|
814
|
+
</tbody>
|
|
815
|
+
</table>
|
|
816
|
+
</div>
|
|
817
|
+
)
|
|
818
|
+
}
|
|
819
|
+
|
|
633
820
|
function PreviewTable({
|
|
634
821
|
object,
|
|
635
822
|
rows,
|
|
@@ -770,3 +957,139 @@ function Field({
|
|
|
770
957
|
function messageOf(error: Error | null | undefined): string | null {
|
|
771
958
|
return error === null || error === undefined ? null : error.message
|
|
772
959
|
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* One button that fills the workspace with a small demo fixture.
|
|
963
|
+
*
|
|
964
|
+
* The endpoint refuses on a workspace that already has data, so the button
|
|
965
|
+
* never doubles the seed and the "already has data" answer surfaces here as
|
|
966
|
+
* a plain error line.
|
|
967
|
+
*/
|
|
968
|
+
function SampleDataSection(): ReactNode {
|
|
969
|
+
const { session } = useSession()
|
|
970
|
+
const workspaceId = session?.workspaceId ?? null
|
|
971
|
+
const install = useInstallSampleData()
|
|
972
|
+
const [counts, setCounts] = useState<SampleDataCounts | null>(null)
|
|
973
|
+
|
|
974
|
+
function onInstall(): void {
|
|
975
|
+
if (workspaceId === null) {
|
|
976
|
+
return
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
install
|
|
980
|
+
.runAsync({ workspaceId })
|
|
981
|
+
.then((result) => {
|
|
982
|
+
setCounts(result)
|
|
983
|
+
})
|
|
984
|
+
.catch(() => undefined)
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const failure = messageOf(install.error)
|
|
988
|
+
|
|
989
|
+
return (
|
|
990
|
+
<section className="space-y-4">
|
|
991
|
+
<div>
|
|
992
|
+
<h2 className="text-[15px] font-semibold text-ink">Install sample data</h2>
|
|
993
|
+
<p className="mt-1 text-[13px] text-ink-muted">
|
|
994
|
+
A small set of companies, people, and deals so a fresh workspace has something
|
|
995
|
+
to look at. Refuses if this workspace already has CRM data.
|
|
996
|
+
</p>
|
|
997
|
+
</div>
|
|
998
|
+
<button
|
|
999
|
+
type="button"
|
|
1000
|
+
onClick={onInstall}
|
|
1001
|
+
disabled={install.isPending || workspaceId === null || counts !== null}
|
|
1002
|
+
className="rounded-md bg-accent px-3.5 py-2 text-[12px] font-semibold text-accent-fg transition hover:bg-accent-hover disabled:opacity-50"
|
|
1003
|
+
>
|
|
1004
|
+
{counts !== null
|
|
1005
|
+
? 'Installed'
|
|
1006
|
+
: install.isPending
|
|
1007
|
+
? 'Installing…'
|
|
1008
|
+
: 'Install sample data'}
|
|
1009
|
+
</button>
|
|
1010
|
+
{counts !== null ? (
|
|
1011
|
+
<p className="text-[12px] text-ink-muted">
|
|
1012
|
+
Added {counts.companies} companies, {counts.people} people, {counts.positions}{' '}
|
|
1013
|
+
positions, {counts.deals} deals, {counts.opportunities} opportunities,{' '}
|
|
1014
|
+
{counts.raises} raises, {counts.partnerships} partnerships, {counts.roles} roles,{' '}
|
|
1015
|
+
{counts.candidates} candidates, {counts.planItems} plan items, and{' '}
|
|
1016
|
+
{counts.notes} notes.
|
|
1017
|
+
</p>
|
|
1018
|
+
) : null}
|
|
1019
|
+
{failure === null ? null : (
|
|
1020
|
+
<p role="alert" className="text-[12px] font-medium text-danger">
|
|
1021
|
+
{failure}
|
|
1022
|
+
</p>
|
|
1023
|
+
)}
|
|
1024
|
+
</section>
|
|
1025
|
+
)
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* Sweep every workspace Company that carries a domain and attach each Person
|
|
1030
|
+
* whose email is at that domain.
|
|
1031
|
+
*
|
|
1032
|
+
* Backfills links for records that predate the auto-linker or arrived through
|
|
1033
|
+
* a bulk import. Idempotent: a follow-up run adds nothing. Never removes.
|
|
1034
|
+
*/
|
|
1035
|
+
function RelinkEmailDomainsSection(): ReactNode {
|
|
1036
|
+
const { session } = useSession()
|
|
1037
|
+
const workspaceId = session?.workspaceId ?? null
|
|
1038
|
+
const relink = useRelinkEmailDomains()
|
|
1039
|
+
const [outcome, setOutcome] = useState<{
|
|
1040
|
+
readonly companiesScanned: number
|
|
1041
|
+
readonly positionsCreated: number
|
|
1042
|
+
} | null>(null)
|
|
1043
|
+
|
|
1044
|
+
function onRun(): void {
|
|
1045
|
+
if (workspaceId === null) {
|
|
1046
|
+
return
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
relink
|
|
1050
|
+
.runAsync({ workspaceId })
|
|
1051
|
+
.then((result) => {
|
|
1052
|
+
setOutcome({
|
|
1053
|
+
companiesScanned: result.companiesScanned,
|
|
1054
|
+
positionsCreated: result.positionsCreated,
|
|
1055
|
+
})
|
|
1056
|
+
})
|
|
1057
|
+
.catch(() => undefined)
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
const failure = messageOf(relink.error)
|
|
1061
|
+
|
|
1062
|
+
return (
|
|
1063
|
+
<section className="space-y-4 border-t border-border pt-8">
|
|
1064
|
+
<div>
|
|
1065
|
+
<h2 className="text-[15px] font-semibold text-ink">Rebuild email-domain links</h2>
|
|
1066
|
+
<p className="mt-1 text-[13px] text-ink-muted">
|
|
1067
|
+
Attach every workspace Person to a Company whose domain matches their email. Safe to run
|
|
1068
|
+
more than once — a Person already at a Company is left alone, and consumer email hosts
|
|
1069
|
+
(gmail, hotmail, …) are skipped.
|
|
1070
|
+
</p>
|
|
1071
|
+
</div>
|
|
1072
|
+
<button
|
|
1073
|
+
type="button"
|
|
1074
|
+
onClick={onRun}
|
|
1075
|
+
disabled={relink.isPending || workspaceId === null}
|
|
1076
|
+
className="rounded-md bg-accent px-3.5 py-2 text-[12px] font-semibold text-accent-fg transition hover:bg-accent-hover disabled:opacity-50"
|
|
1077
|
+
>
|
|
1078
|
+
{relink.isPending ? 'Rebuilding…' : 'Rebuild links'}
|
|
1079
|
+
</button>
|
|
1080
|
+
{outcome !== null ? (
|
|
1081
|
+
<p className="text-[12px] text-ink-muted">
|
|
1082
|
+
Scanned {outcome.companiesScanned}{' '}
|
|
1083
|
+
{outcome.companiesScanned === 1 ? 'company' : 'companies'}, added{' '}
|
|
1084
|
+
{outcome.positionsCreated}{' '}
|
|
1085
|
+
{outcome.positionsCreated === 1 ? 'position' : 'positions'}.
|
|
1086
|
+
</p>
|
|
1087
|
+
) : null}
|
|
1088
|
+
{failure === null ? null : (
|
|
1089
|
+
<p role="alert" className="text-[12px] font-medium text-danger">
|
|
1090
|
+
{failure}
|
|
1091
|
+
</p>
|
|
1092
|
+
)}
|
|
1093
|
+
</section>
|
|
1094
|
+
)
|
|
1095
|
+
}
|
|
@@ -7,6 +7,7 @@ import { Chip } from '../../components/Chip.tsx'
|
|
|
7
7
|
import type { ChipTone } from '../../components/Chip.tsx'
|
|
8
8
|
import { DataTable } from '../../components/DataTable.tsx'
|
|
9
9
|
import type { Column } from '../../components/DataTable.tsx'
|
|
10
|
+
import { Paginator } from '../../components/Paginator.tsx'
|
|
10
11
|
import { ErrorPanel, LoadingPanel } from '../../components/QueryState.tsx'
|
|
11
12
|
import { SegmentedControl } from '../../components/SegmentedControl.tsx'
|
|
12
13
|
import { formatDateTime } from '../../lib/dates.ts'
|
|
@@ -138,25 +139,19 @@ export function WebhookDeliveries({ webhookId }: { readonly webhookId: string })
|
|
|
138
139
|
{status === 'all' ? 'Nothing delivered yet.' : `No ${status} deliveries.`}
|
|
139
140
|
</p>
|
|
140
141
|
) : (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
142
|
+
<>
|
|
143
|
+
<Paginator list={deliveries} placement="top" />
|
|
144
|
+
<DataTable
|
|
145
|
+
columns={columns}
|
|
146
|
+
rows={deliveries.records}
|
|
147
|
+
getRowId={(delivery) => delivery.id}
|
|
148
|
+
/>
|
|
149
|
+
</>
|
|
146
150
|
)}
|
|
147
151
|
|
|
148
152
|
{open !== undefined && <PayloadPanel delivery={open} />}
|
|
149
153
|
|
|
150
|
-
{deliveries
|
|
151
|
-
<button
|
|
152
|
-
type="button"
|
|
153
|
-
onClick={deliveries.loadMore}
|
|
154
|
-
disabled={deliveries.isLoadingMore}
|
|
155
|
-
className="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"
|
|
156
|
-
>
|
|
157
|
-
{deliveries.isLoadingMore ? 'Loading…' : 'Load more'}
|
|
158
|
-
</button>
|
|
159
|
-
)}
|
|
154
|
+
<Paginator list={deliveries} />
|
|
160
155
|
</div>
|
|
161
156
|
)
|
|
162
157
|
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import { Chip } from '../../components/Chip.tsx'
|
|
20
20
|
import type { ChipTone } from '../../components/Chip.tsx'
|
|
21
21
|
import { PageHeader } from '../../components/PageHeader.tsx'
|
|
22
|
+
import { Paginator } from '../../components/Paginator.tsx'
|
|
22
23
|
import { ErrorPanel, LoadingPanel } from '../../components/QueryState.tsx'
|
|
23
24
|
import { formatDateTime } from '../../lib/dates.ts'
|
|
24
25
|
import { WebhookDeliveries } from './WebhookDeliveries.tsx'
|
|
@@ -68,7 +69,7 @@ function MemberNotice(): React.JSX.Element {
|
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
function WebhookAdmin(): React.JSX.Element {
|
|
71
|
-
const
|
|
72
|
+
const webhooks = useWebhooks()
|
|
72
73
|
const [minted, setMinted] = useState<CreatedWebhook | null>(null)
|
|
73
74
|
|
|
74
75
|
return (
|
|
@@ -88,30 +89,24 @@ function WebhookAdmin(): React.JSX.Element {
|
|
|
88
89
|
/>
|
|
89
90
|
)}
|
|
90
91
|
|
|
91
|
-
{error !== null && <ErrorPanel error={error} />}
|
|
92
|
+
{webhooks.error !== null && <ErrorPanel error={webhooks.error} />}
|
|
92
93
|
|
|
93
|
-
{isLoading ? (
|
|
94
|
+
{webhooks.isLoading ? (
|
|
94
95
|
<LoadingPanel label="Loading webhooks…" />
|
|
95
|
-
) : records.length === 0 ? (
|
|
96
|
+
) : webhooks.records.length === 0 ? (
|
|
96
97
|
<p className="text-[13px] text-ink-muted">No webhooks yet.</p>
|
|
97
98
|
) : (
|
|
98
|
-
|
|
99
|
-
{
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
<>
|
|
100
|
+
<Paginator list={webhooks} placement="top" />
|
|
101
|
+
<ul className="space-y-3">
|
|
102
|
+
{webhooks.records.map((webhook) => (
|
|
103
|
+
<WebhookRow key={webhook.id} webhook={webhook} onRotated={setMinted} />
|
|
104
|
+
))}
|
|
105
|
+
</ul>
|
|
106
|
+
</>
|
|
103
107
|
)}
|
|
104
108
|
|
|
105
|
-
{
|
|
106
|
-
<button
|
|
107
|
-
type="button"
|
|
108
|
-
disabled={isLoadingMore}
|
|
109
|
-
onClick={loadMore}
|
|
110
|
-
className="rounded-md border border-border px-3 py-1.5 text-[12px] font-medium text-ink-muted transition hover:text-ink disabled:opacity-50"
|
|
111
|
-
>
|
|
112
|
-
{isLoadingMore ? 'Loading…' : 'Load more'}
|
|
113
|
-
</button>
|
|
114
|
-
)}
|
|
109
|
+
<Paginator list={webhooks} />
|
|
115
110
|
</>
|
|
116
111
|
)
|
|
117
112
|
}
|