@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
|
@@ -30,6 +30,7 @@ import { DecisionsPanel } from '../components/DecisionsPanel.tsx'
|
|
|
30
30
|
import { DeleteRecord } from '../components/DeleteRecord.tsx'
|
|
31
31
|
import { EntitySearch } from '../components/EntitySearch.tsx'
|
|
32
32
|
import { InlineEdit } from '../components/InlineEdit.tsx'
|
|
33
|
+
import { ListsPanel } from '../components/ListsPanel.tsx'
|
|
33
34
|
import { NotesPanel } from '../components/NotesPanel.tsx'
|
|
34
35
|
import { RelatedPlanAttention } from '../components/PlanAttention.tsx'
|
|
35
36
|
import { ErrorPanel, LoadingPanel, NotFoundPanel } from '../components/QueryState.tsx'
|
|
@@ -45,7 +46,7 @@ import { toOptions, toTags } from './fields.ts'
|
|
|
45
46
|
/**
|
|
46
47
|
* One company.
|
|
47
48
|
*
|
|
48
|
-
* Overview, Activity, Notes and
|
|
49
|
+
* Overview, Activity, Notes, Decisions, Lists and People render today. The
|
|
49
50
|
* tabs for Deals, Opportunities, Partnerships and Raises wait for their
|
|
50
51
|
* endpoints. A UI module can add its own through the `company` record-tab slot.
|
|
51
52
|
*/
|
|
@@ -85,8 +86,10 @@ export function CompanyDetail(): React.JSX.Element {
|
|
|
85
86
|
const tabs: readonly RecordTabDescriptor<string>[] = [
|
|
86
87
|
{ id: 'overview', label: 'Overview' },
|
|
87
88
|
{ id: 'activity', label: 'Activity' },
|
|
89
|
+
{ id: 'people', label: 'People' },
|
|
88
90
|
{ id: 'notes', label: 'Notes' },
|
|
89
91
|
{ id: 'decisions', label: 'Decisions' },
|
|
92
|
+
{ id: 'lists', label: 'Lists' },
|
|
90
93
|
...moduleTabs.map((tab) => ({ id: tab.id, label: tab.label })),
|
|
91
94
|
]
|
|
92
95
|
const active = tabs.some((tab) => tab.id === activeTab) ? activeTab : 'overview'
|
|
@@ -129,15 +132,16 @@ export function CompanyDetail(): React.JSX.Element {
|
|
|
129
132
|
>
|
|
130
133
|
{active === 'overview' && <CompanyOverview company={record} />}
|
|
131
134
|
{active === 'activity' && <ActivitiesPanel targetType="company" targetId={record.id} />}
|
|
135
|
+
{active === 'people' && <CompanyPeople company={record} />}
|
|
132
136
|
{active === 'notes' && <NotesPanel targetType="company" targetId={record.id} />}
|
|
133
137
|
{active === 'decisions' && <DecisionsPanel targetType="company" targetId={record.id} />}
|
|
138
|
+
{active === 'lists' && <ListsPanel targetType="company" targetId={record.id} />}
|
|
134
139
|
{moduleTab?.render({ objectType: 'company', recordId: record.id })}
|
|
135
140
|
</RecordTabs>
|
|
136
141
|
</div>
|
|
137
142
|
|
|
138
143
|
<aside className="space-y-4 text-[12px] lg:sticky lg:top-6">
|
|
139
144
|
<CompanySidebar company={record} />
|
|
140
|
-
<CompanyPeople company={record} />
|
|
141
145
|
</aside>
|
|
142
146
|
</div>
|
|
143
147
|
</div>
|
|
@@ -380,7 +384,7 @@ function CompanyPeople({ company }: { readonly company: Company }): React.JSX.El
|
|
|
380
384
|
function submit(event: FormEvent): void {
|
|
381
385
|
event.preventDefault()
|
|
382
386
|
|
|
383
|
-
if (personId.length === 0
|
|
387
|
+
if (personId.length === 0) {
|
|
384
388
|
return
|
|
385
389
|
}
|
|
386
390
|
|
|
@@ -389,73 +393,28 @@ function CompanyPeople({ company }: { readonly company: Company }): React.JSX.El
|
|
|
389
393
|
}
|
|
390
394
|
|
|
391
395
|
return (
|
|
392
|
-
<section
|
|
393
|
-
<
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
compact
|
|
401
|
-
/>
|
|
402
|
-
</div>
|
|
396
|
+
<section>
|
|
397
|
+
<SectionHeader
|
|
398
|
+
title="People"
|
|
399
|
+
onAdd={() => {
|
|
400
|
+
setAdding((current) => !current)
|
|
401
|
+
}}
|
|
402
|
+
addLabel="Add person"
|
|
403
|
+
/>
|
|
403
404
|
|
|
404
405
|
{createPosition.error !== null && (
|
|
405
|
-
<div className="
|
|
406
|
+
<div className="mb-3">
|
|
406
407
|
<ErrorPanel error={createPosition.error} />
|
|
407
408
|
</div>
|
|
408
409
|
)}
|
|
409
410
|
{deletePosition.error !== null && (
|
|
410
|
-
<div className="
|
|
411
|
+
<div className="mb-3">
|
|
411
412
|
<ErrorPanel error={deletePosition.error} />
|
|
412
413
|
</div>
|
|
413
414
|
)}
|
|
414
415
|
|
|
415
|
-
<ul className="divide-y divide-border">
|
|
416
|
-
{positions.isLoading && (
|
|
417
|
-
<li className="px-3.5 py-4 text-[12px] text-ink-faint">Loading people…</li>
|
|
418
|
-
)}
|
|
419
|
-
{!positions.isLoading && positions.records.length === 0 && !adding && (
|
|
420
|
-
<li className="px-3.5 py-4 text-[12px] text-ink-faint">No people yet.</li>
|
|
421
|
-
)}
|
|
422
|
-
{positions.records.map((position) => {
|
|
423
|
-
const person = personById.get(position.personId)
|
|
424
|
-
|
|
425
|
-
return (
|
|
426
|
-
<li key={position.id} className="space-y-1 px-3.5 py-2.5">
|
|
427
|
-
<div className="flex items-start justify-between gap-2">
|
|
428
|
-
<Link
|
|
429
|
-
to={`/people/${position.personId}`}
|
|
430
|
-
className="text-[13px] font-medium text-ink hover:text-accent"
|
|
431
|
-
>
|
|
432
|
-
{person?.name ?? 'Unknown'}
|
|
433
|
-
</Link>
|
|
434
|
-
<button
|
|
435
|
-
type="button"
|
|
436
|
-
onClick={() => {
|
|
437
|
-
deletePosition.run(position.id)
|
|
438
|
-
}}
|
|
439
|
-
className="text-[11px] font-medium text-danger hover:underline"
|
|
440
|
-
>
|
|
441
|
-
Remove
|
|
442
|
-
</button>
|
|
443
|
-
</div>
|
|
444
|
-
<InlineEdit
|
|
445
|
-
value={position.title}
|
|
446
|
-
onChange={(next) => {
|
|
447
|
-
updateTitle.run({ id: position.id, changes: { title: next } })
|
|
448
|
-
}}
|
|
449
|
-
displayClassName="not-italic text-[12px]"
|
|
450
|
-
emptyLabel="Add position title…"
|
|
451
|
-
/>
|
|
452
|
-
</li>
|
|
453
|
-
)
|
|
454
|
-
})}
|
|
455
|
-
</ul>
|
|
456
|
-
|
|
457
416
|
{adding && (
|
|
458
|
-
<form onSubmit={submit} className="space-y-2
|
|
417
|
+
<form onSubmit={submit} className="mb-4 space-y-2 rounded-md border border-border bg-surface p-3">
|
|
459
418
|
<EntitySearch
|
|
460
419
|
options={searchable.records
|
|
461
420
|
.filter((person) => !held.has(person.id))
|
|
@@ -476,9 +435,8 @@ function CompanyPeople({ company }: { readonly company: Company }): React.JSX.El
|
|
|
476
435
|
onChange={(event) => {
|
|
477
436
|
setTitle(event.target.value)
|
|
478
437
|
}}
|
|
479
|
-
placeholder="Position title"
|
|
480
|
-
className="w-full rounded-md border border-border bg-surface-raised px-2 py-1.5 text-[
|
|
481
|
-
required
|
|
438
|
+
placeholder="Position title (optional)"
|
|
439
|
+
className="w-full rounded-md border border-border bg-surface-raised px-2 py-1.5 text-[13px] outline-none focus:border-accent"
|
|
482
440
|
/>
|
|
483
441
|
<div className="flex justify-end gap-2">
|
|
484
442
|
<button
|
|
@@ -497,6 +455,48 @@ function CompanyPeople({ company }: { readonly company: Company }): React.JSX.El
|
|
|
497
455
|
</div>
|
|
498
456
|
</form>
|
|
499
457
|
)}
|
|
458
|
+
|
|
459
|
+
{positions.isLoading && <p className="text-[13px] text-ink-faint">Loading people…</p>}
|
|
460
|
+
|
|
461
|
+
{!positions.isLoading && positions.records.length === 0 && !adding ? (
|
|
462
|
+
<p className="text-[13px] text-ink-faint">No people yet.</p>
|
|
463
|
+
) : (
|
|
464
|
+
<ul className="divide-y divide-border rounded-md border border-border">
|
|
465
|
+
{positions.records.map((position) => {
|
|
466
|
+
const person = personById.get(position.personId)
|
|
467
|
+
|
|
468
|
+
return (
|
|
469
|
+
<li key={position.id} className="space-y-1 px-3.5 py-3">
|
|
470
|
+
<div className="flex items-start justify-between gap-2">
|
|
471
|
+
<Link
|
|
472
|
+
to={`/people/${position.personId}`}
|
|
473
|
+
className="text-[13px] font-medium text-ink hover:text-accent"
|
|
474
|
+
>
|
|
475
|
+
{person?.name ?? 'Unknown'}
|
|
476
|
+
</Link>
|
|
477
|
+
<button
|
|
478
|
+
type="button"
|
|
479
|
+
onClick={() => {
|
|
480
|
+
deletePosition.run(position.id)
|
|
481
|
+
}}
|
|
482
|
+
className="text-[11px] font-medium text-danger hover:underline"
|
|
483
|
+
>
|
|
484
|
+
Remove
|
|
485
|
+
</button>
|
|
486
|
+
</div>
|
|
487
|
+
<InlineEdit
|
|
488
|
+
value={position.title}
|
|
489
|
+
onChange={(next) => {
|
|
490
|
+
updateTitle.run({ id: position.id, changes: { title: next } })
|
|
491
|
+
}}
|
|
492
|
+
displayClassName="not-italic text-[12px] text-ink-muted"
|
|
493
|
+
emptyLabel="Add position title…"
|
|
494
|
+
/>
|
|
495
|
+
</li>
|
|
496
|
+
)
|
|
497
|
+
})}
|
|
498
|
+
</ul>
|
|
499
|
+
)}
|
|
500
500
|
</section>
|
|
501
501
|
)
|
|
502
502
|
}
|
package/src/pages/DealDetail.tsx
CHANGED
|
@@ -19,6 +19,7 @@ import { DecisionsPanel } from '../components/DecisionsPanel.tsx'
|
|
|
19
19
|
import { DeleteRecord } from '../components/DeleteRecord.tsx'
|
|
20
20
|
import { EntitySearch } from '../components/EntitySearch.tsx'
|
|
21
21
|
import { InlineEdit } from '../components/InlineEdit.tsx'
|
|
22
|
+
import { ListsPanel } from '../components/ListsPanel.tsx'
|
|
22
23
|
import { NotesPanel } from '../components/NotesPanel.tsx'
|
|
23
24
|
import { PlanAttention } from '../components/PlanAttention.tsx'
|
|
24
25
|
import { PlanPanel } from '../components/PlanPanel.tsx'
|
|
@@ -73,6 +74,7 @@ export function DealDetail(): React.JSX.Element {
|
|
|
73
74
|
{ id: 'activity', label: 'Activity' },
|
|
74
75
|
{ id: 'notes', label: 'Notes' },
|
|
75
76
|
{ id: 'decisions', label: 'Decisions' },
|
|
77
|
+
{ id: 'lists', label: 'Lists' },
|
|
76
78
|
...moduleTabs.map((tab) => ({ id: tab.id, label: tab.label })),
|
|
77
79
|
]
|
|
78
80
|
const active = tabs.some((tab) => tab.id === activeTab) ? activeTab : 'overview'
|
|
@@ -113,6 +115,7 @@ export function DealDetail(): React.JSX.Element {
|
|
|
113
115
|
{active === 'activity' && <ActivitiesPanel targetType="deal" targetId={record.id} />}
|
|
114
116
|
{active === 'notes' && <NotesPanel targetType="deal" targetId={record.id} />}
|
|
115
117
|
{active === 'decisions' && <DecisionsPanel targetType="deal" targetId={record.id} />}
|
|
118
|
+
{active === 'lists' && <ListsPanel targetType="deal" targetId={record.id} />}
|
|
116
119
|
{moduleTab?.render({ objectType: 'deal', recordId: record.id })}
|
|
117
120
|
</RecordTabs>
|
|
118
121
|
</div>
|
package/src/pages/DealsPage.tsx
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Deal, 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'
|
|
@@ -7,17 +6,22 @@ import { useDeals, useCreateDeal, useUpdateDeal } from '../api/resources/deals.t
|
|
|
7
6
|
import { useMembers } from '../api/resources/members.ts'
|
|
8
7
|
import { usePipelineStages } from '../api/resources/pipelineStages.ts'
|
|
9
8
|
import { MAX_PAGE_SIZE, usePlanItems } from '../api/resources/planItems.ts'
|
|
9
|
+
import { useTimezone } from '../api/resources/account.ts'
|
|
10
10
|
import { Chip } from '../components/Chip.tsx'
|
|
11
11
|
import type { ChipTone } from '../components/Chip.tsx'
|
|
12
|
+
import { ColumnPicker } from '../components/ColumnPicker.tsx'
|
|
12
13
|
import { DataTable } from '../components/DataTable.tsx'
|
|
13
14
|
import type { Column, DataTableGroup } from '../components/DataTable.tsx'
|
|
14
15
|
import { KanbanBoard } from '../components/KanbanBoard.tsx'
|
|
15
16
|
import { PageHeader } from '../components/PageHeader.tsx'
|
|
17
|
+
import { Paginator } from '../components/Paginator.tsx'
|
|
16
18
|
import { ErrorPanel, LoadingPanel } from '../components/QueryState.tsx'
|
|
17
19
|
import { SegmentedControl } from '../components/SegmentedControl.tsx'
|
|
18
|
-
import { formatDay } from '../lib/dates.ts'
|
|
20
|
+
import { formatDate, formatDay } from '../lib/dates.ts'
|
|
21
|
+
import { useListView } from '../lib/listView.ts'
|
|
19
22
|
import { formatMoney } from '../lib/money.ts'
|
|
20
23
|
import { DUE_BUCKETS, byDateThenTitle, dueBucketFor, nextOpenByTarget } from '../lib/plan.ts'
|
|
24
|
+
import { serverSortOnly } from '../lib/sort.ts'
|
|
21
25
|
|
|
22
26
|
/**
|
|
23
27
|
* The Deals pipeline: a board and a list over the same records, grouped by stage
|
|
@@ -31,6 +35,17 @@ type BoardView = 'list' | 'columns'
|
|
|
31
35
|
type PipelineScope = 'open' | 'all'
|
|
32
36
|
type ListGrouping = 'stage' | 'due'
|
|
33
37
|
|
|
38
|
+
const SCOPES: readonly PipelineScope[] = ['open', 'all']
|
|
39
|
+
const GROUPINGS: readonly ListGrouping[] = ['stage', 'due']
|
|
40
|
+
|
|
41
|
+
function readScope(stored: string | undefined): PipelineScope {
|
|
42
|
+
return SCOPES.includes(stored as PipelineScope) ? (stored as PipelineScope) : 'open'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readGrouping(stored: string | undefined): ListGrouping {
|
|
46
|
+
return GROUPINGS.includes(stored as ListGrouping) ? (stored as ListGrouping) : 'stage'
|
|
47
|
+
}
|
|
48
|
+
|
|
34
49
|
/** Tones keyed by the seeded slugs. A renamed stage keeps its slug and its tone. */
|
|
35
50
|
const STAGE_TONES: Readonly<Record<string, ChipTone>> = {
|
|
36
51
|
won: 'success',
|
|
@@ -43,19 +58,54 @@ function stageTone(stage: PipelineStage): ChipTone {
|
|
|
43
58
|
return STAGE_TONES[stage.slug] ?? 'neutral'
|
|
44
59
|
}
|
|
45
60
|
|
|
61
|
+
const DEFAULT_VISIBLE_KEYS: readonly string[] = [
|
|
62
|
+
'name',
|
|
63
|
+
'company',
|
|
64
|
+
'value',
|
|
65
|
+
'nextPlan',
|
|
66
|
+
'owner',
|
|
67
|
+
'close',
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
const SERVER_SORT_KEYS: readonly string[] = ['name', 'created_at', 'updated_at']
|
|
71
|
+
|
|
46
72
|
export function DealsPage(): React.JSX.Element {
|
|
47
73
|
const navigate = useNavigate()
|
|
48
|
-
const [view, setView] = useState<BoardView>('list')
|
|
49
|
-
const [scope, setScope] = useState<PipelineScope>('open')
|
|
50
|
-
const [grouping, setGrouping] = useState<ListGrouping>('stage')
|
|
51
|
-
const [sort, setSort] = useState<string | undefined>(undefined)
|
|
52
74
|
|
|
53
75
|
const stages = usePipelineStages('deal')
|
|
54
|
-
const deals = useDeals({ sort })
|
|
55
76
|
const companies = useCompanies({ limit: 200 })
|
|
77
|
+
// The list-view controller holds every persisted knob, so it is declared
|
|
78
|
+
// before the deals query — the sort it stores decides which page comes
|
|
79
|
+
// back. Board vs list defaults to the board here; the other three read
|
|
80
|
+
// their own default from the readers above.
|
|
81
|
+
const supportedKeys: readonly string[] = [
|
|
82
|
+
'name',
|
|
83
|
+
'company',
|
|
84
|
+
'stage',
|
|
85
|
+
'value',
|
|
86
|
+
'currency',
|
|
87
|
+
'nextPlan',
|
|
88
|
+
'owner',
|
|
89
|
+
'close',
|
|
90
|
+
'competitors',
|
|
91
|
+
'tags',
|
|
92
|
+
'summary',
|
|
93
|
+
'risks',
|
|
94
|
+
'whyWin',
|
|
95
|
+
'externalId',
|
|
96
|
+
'createdAt',
|
|
97
|
+
'updatedAt',
|
|
98
|
+
]
|
|
99
|
+
const listView = useListView('deals', supportedKeys, DEFAULT_VISIBLE_KEYS)
|
|
100
|
+
const view: BoardView = listView.mode ?? 'columns'
|
|
101
|
+
const scope = readScope(listView.scope)
|
|
102
|
+
const grouping = readGrouping(listView.grouping)
|
|
103
|
+
const sort = listView.sort
|
|
104
|
+
const deals = useDeals({ sort: serverSortOnly(sort, SERVER_SORT_KEYS) })
|
|
56
105
|
const members = useMembers()
|
|
57
106
|
const createDeal = useCreateDeal()
|
|
58
107
|
const updateDeal = useUpdateDeal()
|
|
108
|
+
const timezone = useTimezone()
|
|
59
109
|
|
|
60
110
|
const allStages = [...stages.records].sort((a, b) => a.sortOrder - b.sortOrder)
|
|
61
111
|
const visibleStages = scope === 'open' ? allStages.filter((stage) => stage.open) : allStages
|
|
@@ -71,7 +121,7 @@ export function DealsPage(): React.JSX.Element {
|
|
|
71
121
|
{ enabled: askedDealIds.length > 0 },
|
|
72
122
|
)
|
|
73
123
|
const nextPlanByDeal = nextOpenByTarget(planItems.records)
|
|
74
|
-
const plansTruncated = visibleDeals.length > MAX_PAGE_SIZE || planItems.
|
|
124
|
+
const plansTruncated = visibleDeals.length > MAX_PAGE_SIZE || planItems.hasNext
|
|
75
125
|
|
|
76
126
|
const companyNameById = new Map(companies.records.map((company) => [company.id, company.name]))
|
|
77
127
|
|
|
@@ -87,6 +137,8 @@ export function DealsPage(): React.JSX.Element {
|
|
|
87
137
|
await navigate(`/deals/${deal.id}`)
|
|
88
138
|
}
|
|
89
139
|
|
|
140
|
+
const stageLabelById = new Map(allStages.map((stage) => [stage.id, stage.label]))
|
|
141
|
+
|
|
90
142
|
const columns: readonly Column<Deal>[] = [
|
|
91
143
|
{
|
|
92
144
|
key: 'name',
|
|
@@ -97,21 +149,36 @@ export function DealsPage(): React.JSX.Element {
|
|
|
97
149
|
{
|
|
98
150
|
key: 'company',
|
|
99
151
|
header: 'Company',
|
|
152
|
+
getSortValue: (deal) => companyNameById.get(deal.companyId) ?? null,
|
|
100
153
|
render: (deal) => companyNameById.get(deal.companyId) ?? '—',
|
|
101
154
|
},
|
|
155
|
+
{
|
|
156
|
+
key: 'stage',
|
|
157
|
+
header: 'Stage',
|
|
158
|
+
getSortValue: (deal) => stageLabelById.get(deal.stageId) ?? null,
|
|
159
|
+
render: (deal) => stageLabelById.get(deal.stageId) ?? '—',
|
|
160
|
+
},
|
|
102
161
|
{
|
|
103
162
|
key: 'value',
|
|
104
163
|
header: 'Value',
|
|
105
164
|
className: 'w-28',
|
|
165
|
+
getSortValue: (deal) => deal.valueCents,
|
|
106
166
|
render: (deal) => (
|
|
107
167
|
<span className="font-mono text-[12px]">
|
|
108
168
|
{deal.valueCents === null ? '—' : formatMoney(deal.valueCents, deal.currency)}
|
|
109
169
|
</span>
|
|
110
170
|
),
|
|
111
171
|
},
|
|
172
|
+
{
|
|
173
|
+
key: 'currency',
|
|
174
|
+
header: 'Currency',
|
|
175
|
+
getSortValue: (deal) => deal.currency,
|
|
176
|
+
render: (deal) => deal.currency ?? '—',
|
|
177
|
+
},
|
|
112
178
|
{
|
|
113
179
|
key: 'nextPlan',
|
|
114
180
|
header: 'Next plan',
|
|
181
|
+
getSortValue: (deal) => nextPlanByDeal.get(deal.id)?.date ?? null,
|
|
115
182
|
render: (deal) => {
|
|
116
183
|
const next = nextPlanByDeal.get(deal.id)
|
|
117
184
|
|
|
@@ -132,6 +199,8 @@ export function DealsPage(): React.JSX.Element {
|
|
|
132
199
|
{
|
|
133
200
|
key: 'owner',
|
|
134
201
|
header: 'Owner',
|
|
202
|
+
getSortValue: (deal) =>
|
|
203
|
+
deal.ownerId === null ? null : (members.nameById.get(deal.ownerId) ?? 'Unknown'),
|
|
135
204
|
render: (deal) =>
|
|
136
205
|
deal.ownerId === null ? '—' : (members.nameById.get(deal.ownerId) ?? 'Unknown'),
|
|
137
206
|
},
|
|
@@ -139,14 +208,91 @@ export function DealsPage(): React.JSX.Element {
|
|
|
139
208
|
key: 'close',
|
|
140
209
|
header: 'Close',
|
|
141
210
|
className: 'w-28',
|
|
211
|
+
getSortValue: (deal) => deal.expectedClose,
|
|
142
212
|
render: (deal) => (
|
|
143
213
|
<span className="font-mono text-[12px] text-ink-muted">
|
|
144
214
|
{deal.expectedClose === null ? '—' : formatDay(deal.expectedClose)}
|
|
145
215
|
</span>
|
|
146
216
|
),
|
|
147
217
|
},
|
|
218
|
+
{
|
|
219
|
+
key: 'competitors',
|
|
220
|
+
header: 'Competitors',
|
|
221
|
+
getSortValue: (deal) => deal.competitors.join(', ') || null,
|
|
222
|
+
render: (deal) =>
|
|
223
|
+
deal.competitors.length === 0 ? '—' : (
|
|
224
|
+
<span className="flex flex-wrap gap-1">
|
|
225
|
+
{deal.competitors.map((entry) => (
|
|
226
|
+
<Chip key={entry}>
|
|
227
|
+
<span className="text-[10px]">{entry}</span>
|
|
228
|
+
</Chip>
|
|
229
|
+
))}
|
|
230
|
+
</span>
|
|
231
|
+
),
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
key: 'tags',
|
|
235
|
+
header: 'Tags',
|
|
236
|
+
getSortValue: (deal) => deal.tags.join(', ') || null,
|
|
237
|
+
render: (deal) =>
|
|
238
|
+
deal.tags.length === 0 ? '—' : (
|
|
239
|
+
<span className="flex flex-wrap gap-1">
|
|
240
|
+
{deal.tags.map((tag) => (
|
|
241
|
+
<Chip key={tag}>
|
|
242
|
+
<span className="text-[10px]">{tag}</span>
|
|
243
|
+
</Chip>
|
|
244
|
+
))}
|
|
245
|
+
</span>
|
|
246
|
+
),
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
key: 'summary',
|
|
250
|
+
header: 'Summary',
|
|
251
|
+
getSortValue: (deal) => deal.summary || null,
|
|
252
|
+
render: (deal) =>
|
|
253
|
+
deal.summary.length === 0 ? '—' : <span className="text-ink-muted">{deal.summary}</span>,
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
key: 'risks',
|
|
257
|
+
header: 'Risks',
|
|
258
|
+
getSortValue: (deal) => deal.risks || null,
|
|
259
|
+
render: (deal) =>
|
|
260
|
+
deal.risks.length === 0 ? '—' : <span className="text-ink-muted">{deal.risks}</span>,
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
key: 'whyWin',
|
|
264
|
+
header: 'Why win',
|
|
265
|
+
getSortValue: (deal) => deal.whyWin || null,
|
|
266
|
+
render: (deal) =>
|
|
267
|
+
deal.whyWin.length === 0 ? '—' : <span className="text-ink-muted">{deal.whyWin}</span>,
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
key: 'externalId',
|
|
271
|
+
header: 'External ID',
|
|
272
|
+
getSortValue: (deal) => deal.externalId,
|
|
273
|
+
render: (deal) =>
|
|
274
|
+
deal.externalId === null ? (
|
|
275
|
+
'—'
|
|
276
|
+
) : (
|
|
277
|
+
<span className="font-mono text-[12px] text-ink-muted">{deal.externalId}</span>
|
|
278
|
+
),
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
key: 'createdAt',
|
|
282
|
+
header: 'Created',
|
|
283
|
+
sortKey: 'created_at',
|
|
284
|
+
render: (deal) => formatDate(deal.createdAt, timezone),
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
key: 'updatedAt',
|
|
288
|
+
header: 'Updated',
|
|
289
|
+
sortKey: 'updated_at',
|
|
290
|
+
render: (deal) => formatDate(deal.updatedAt, timezone),
|
|
291
|
+
},
|
|
148
292
|
]
|
|
149
293
|
|
|
294
|
+
const pickerOptions = columns.map((column) => ({ key: column.key, label: column.header }))
|
|
295
|
+
|
|
150
296
|
const stageGroups: readonly DataTableGroup<Deal>[] = visibleStages.map((stage) => ({
|
|
151
297
|
id: stage.id,
|
|
152
298
|
label: <Chip tone={stageTone(stage)}>{stage.label}</Chip>,
|
|
@@ -208,7 +354,9 @@ export function DealsPage(): React.JSX.Element {
|
|
|
208
354
|
<SegmentedControl
|
|
209
355
|
ariaLabel="Stage scope"
|
|
210
356
|
value={scope}
|
|
211
|
-
onChange={
|
|
357
|
+
onChange={(next) => {
|
|
358
|
+
listView.setScope(next)
|
|
359
|
+
}}
|
|
212
360
|
options={[
|
|
213
361
|
{ id: 'open', label: 'Open' },
|
|
214
362
|
{ id: 'all', label: 'All' },
|
|
@@ -218,7 +366,9 @@ export function DealsPage(): React.JSX.Element {
|
|
|
218
366
|
<SegmentedControl
|
|
219
367
|
ariaLabel="Group the list"
|
|
220
368
|
value={grouping}
|
|
221
|
-
onChange={
|
|
369
|
+
onChange={(next) => {
|
|
370
|
+
listView.setGrouping(next)
|
|
371
|
+
}}
|
|
222
372
|
options={[
|
|
223
373
|
{ id: 'stage', label: 'By stage' },
|
|
224
374
|
{ id: 'due', label: 'By due' },
|
|
@@ -228,12 +378,21 @@ export function DealsPage(): React.JSX.Element {
|
|
|
228
378
|
<SegmentedControl
|
|
229
379
|
ariaLabel="Board or list"
|
|
230
380
|
value={view}
|
|
231
|
-
onChange={
|
|
381
|
+
onChange={(next) => {
|
|
382
|
+
listView.setMode(next)
|
|
383
|
+
}}
|
|
232
384
|
options={[
|
|
233
385
|
{ id: 'list', label: 'List' },
|
|
234
386
|
{ id: 'columns', label: 'Board' },
|
|
235
387
|
]}
|
|
236
388
|
/>
|
|
389
|
+
{view === 'list' && (
|
|
390
|
+
<ColumnPicker
|
|
391
|
+
options={pickerOptions}
|
|
392
|
+
visibleKeys={listView.visibleKeys}
|
|
393
|
+
onChange={listView.setVisibleKeys}
|
|
394
|
+
/>
|
|
395
|
+
)}
|
|
237
396
|
</>
|
|
238
397
|
}
|
|
239
398
|
/>
|
|
@@ -255,6 +414,7 @@ export function DealsPage(): React.JSX.Element {
|
|
|
255
414
|
<LoadingPanel label="Loading deals…" />
|
|
256
415
|
) : (
|
|
257
416
|
<>
|
|
417
|
+
{view === 'list' && <Paginator list={deals} placement="top" />}
|
|
258
418
|
{view === 'columns' ? (
|
|
259
419
|
<KanbanBoard
|
|
260
420
|
stages={visibleStages.map((stage) => ({ id: stage.id, label: stage.label }))}
|
|
@@ -301,13 +461,16 @@ export function DealsPage(): React.JSX.Element {
|
|
|
301
461
|
? {
|
|
302
462
|
label: 'Show all deals',
|
|
303
463
|
onClick: () => {
|
|
304
|
-
setScope('all')
|
|
464
|
+
listView.setScope('all')
|
|
305
465
|
},
|
|
306
466
|
}
|
|
307
467
|
: undefined
|
|
308
468
|
}
|
|
309
469
|
sort={sort}
|
|
310
|
-
onSortChange={
|
|
470
|
+
onSortChange={(next) => {
|
|
471
|
+
listView.setSort(next)
|
|
472
|
+
}}
|
|
473
|
+
visibleColumnKeys={listView.visibleKeys}
|
|
311
474
|
/>
|
|
312
475
|
)}
|
|
313
476
|
{updateDeal.error !== null && (
|
|
@@ -315,17 +478,8 @@ export function DealsPage(): React.JSX.Element {
|
|
|
315
478
|
<ErrorPanel error={updateDeal.error} />
|
|
316
479
|
</div>
|
|
317
480
|
)}
|
|
318
|
-
{
|
|
319
|
-
|
|
320
|
-
type="button"
|
|
321
|
-
onClick={deals.loadMore}
|
|
322
|
-
disabled={deals.isLoadingMore}
|
|
323
|
-
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"
|
|
324
|
-
>
|
|
325
|
-
{deals.isLoadingMore ? 'Loading…' : 'Load more'}
|
|
326
|
-
</button>
|
|
327
|
-
)}
|
|
328
|
-
{companies.hasMore && (
|
|
481
|
+
{view === 'list' && <Paginator list={deals} />}
|
|
482
|
+
{companies.hasNext && (
|
|
329
483
|
<p className="mt-2 text-[11px] text-ink-faint">
|
|
330
484
|
More companies exist than one page returns, so some company names may show as “—”.
|
|
331
485
|
</p>
|