@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,8 @@
|
|
|
1
1
|
import type { ReactNode } from 'react'
|
|
2
2
|
|
|
3
|
+
import { sortRowsBy } from '../lib/sort.ts'
|
|
4
|
+
import type { SortValue } from '../lib/sort.ts'
|
|
5
|
+
|
|
3
6
|
export interface Column<TRow> {
|
|
4
7
|
readonly key: string
|
|
5
8
|
readonly header: string
|
|
@@ -7,6 +10,14 @@ export interface Column<TRow> {
|
|
|
7
10
|
readonly render: (row: TRow) => ReactNode
|
|
8
11
|
/** The `?sort=` field this column drives. Omit for a column with no server-side sort. */
|
|
9
12
|
readonly sortKey?: string | undefined
|
|
13
|
+
/**
|
|
14
|
+
* Returns the value this column sorts by on the client. Set on any column
|
|
15
|
+
* whose resource cannot sort on it server-side (join columns, computed
|
|
16
|
+
* values, fields not in the resource's `_SORTS` map). When the active sort
|
|
17
|
+
* names a `Column.key` with this set, `DataTable` orders the loaded rows in
|
|
18
|
+
* place and does not forward the change to the server.
|
|
19
|
+
*/
|
|
20
|
+
readonly getSortValue?: ((row: TRow) => SortValue) | undefined
|
|
10
21
|
}
|
|
11
22
|
|
|
12
23
|
export interface DataTableGroup<TRow> {
|
|
@@ -35,6 +46,12 @@ export interface DataTableProps<TRow> {
|
|
|
35
46
|
readonly sort?: string | undefined
|
|
36
47
|
/** Fires with the next `?sort=` value when a sortable header is clicked. Required for any column to be clickable. */
|
|
37
48
|
readonly onSortChange?: ((sort: string | undefined) => void) | undefined
|
|
49
|
+
/**
|
|
50
|
+
* Restricts rendering to these column keys, in the order the header uses.
|
|
51
|
+
* Undefined shows every column. An unknown key is ignored rather than raising:
|
|
52
|
+
* a stored preference outliving a rename should hide only what it recognises.
|
|
53
|
+
*/
|
|
54
|
+
readonly visibleColumnKeys?: readonly string[] | undefined
|
|
38
55
|
}
|
|
39
56
|
|
|
40
57
|
type SortDirection = 'asc' | 'desc'
|
|
@@ -62,6 +79,35 @@ function nextSort(sort: string | undefined, field: string): string | undefined {
|
|
|
62
79
|
return current === 'asc' ? `-${field}` : undefined
|
|
63
80
|
}
|
|
64
81
|
|
|
82
|
+
/**
|
|
83
|
+
* The identifier a column contributes to the sort string. Server sorts use the
|
|
84
|
+
* `sortKey` — the field the server knows — so a stored sort keeps working when
|
|
85
|
+
* a column is renamed in the UI. Client-only sorts fall back to the display
|
|
86
|
+
* `key`, since there is no server field to reference.
|
|
87
|
+
*/
|
|
88
|
+
function sortFieldOf<TRow>(column: Column<TRow>): string | undefined {
|
|
89
|
+
if (column.sortKey !== undefined) {
|
|
90
|
+
return column.sortKey
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return column.getSortValue !== undefined ? column.key : undefined
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function findActiveClientColumn<TRow>(
|
|
97
|
+
columns: readonly Column<TRow>[],
|
|
98
|
+
sort: string | undefined,
|
|
99
|
+
): Column<TRow> | undefined {
|
|
100
|
+
if (sort === undefined) {
|
|
101
|
+
return undefined
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const bare = sort.startsWith('-') ? sort.slice(1) : sort
|
|
105
|
+
|
|
106
|
+
return columns.find(
|
|
107
|
+
(column) => column.getSortValue !== undefined && column.sortKey === undefined && column.key === bare,
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
65
111
|
export function DataTable<TRow>({
|
|
66
112
|
columns,
|
|
67
113
|
rows,
|
|
@@ -73,8 +119,34 @@ export function DataTable<TRow>({
|
|
|
73
119
|
getRowId,
|
|
74
120
|
sort,
|
|
75
121
|
onSortChange,
|
|
122
|
+
visibleColumnKeys,
|
|
76
123
|
}: DataTableProps<TRow>): React.JSX.Element {
|
|
77
124
|
const flatRows = groups === undefined ? (rows ?? []) : groups.flatMap((group) => group.rows)
|
|
125
|
+
const displayedColumns =
|
|
126
|
+
visibleColumnKeys === undefined
|
|
127
|
+
? columns
|
|
128
|
+
: columns.filter((column) => visibleColumnKeys.includes(column.key))
|
|
129
|
+
|
|
130
|
+
// Client sort is a page-local reorder: the server-returned page order stays
|
|
131
|
+
// the same, and only what is on screen shifts. Groups sort within each group,
|
|
132
|
+
// because the group is the primary key of the view — mixing rows across
|
|
133
|
+
// groups would break what the group header claims to count.
|
|
134
|
+
const clientSortColumn = findActiveClientColumn(displayedColumns, sort)
|
|
135
|
+
const clientDirection: SortDirection | undefined =
|
|
136
|
+
clientSortColumn === undefined
|
|
137
|
+
? undefined
|
|
138
|
+
: directionOf(sort, clientSortColumn.key) ?? undefined
|
|
139
|
+
const displayedRows =
|
|
140
|
+
clientSortColumn === undefined || clientDirection === undefined || rows === undefined
|
|
141
|
+
? rows
|
|
142
|
+
: sortRowsBy(rows, clientSortColumn.getSortValue!, clientDirection)
|
|
143
|
+
const displayedGroups =
|
|
144
|
+
clientSortColumn === undefined || clientDirection === undefined || groups === undefined
|
|
145
|
+
? groups
|
|
146
|
+
: groups.map((group) => ({
|
|
147
|
+
...group,
|
|
148
|
+
rows: sortRowsBy(group.rows, clientSortColumn.getSortValue!, clientDirection),
|
|
149
|
+
}))
|
|
78
150
|
|
|
79
151
|
if (flatRows.length === 0) {
|
|
80
152
|
return (
|
|
@@ -99,20 +171,21 @@ export function DataTable<TRow>({
|
|
|
99
171
|
<table className="w-full border-collapse text-left text-[13px]">
|
|
100
172
|
<thead>
|
|
101
173
|
<tr className="border-b border-border">
|
|
102
|
-
{
|
|
103
|
-
const
|
|
104
|
-
const
|
|
174
|
+
{displayedColumns.map((column) => {
|
|
175
|
+
const field = sortFieldOf(column)
|
|
176
|
+
const direction = directionOf(sort, field)
|
|
177
|
+
const sortable = field !== undefined && onSortChange !== undefined
|
|
105
178
|
|
|
106
179
|
return (
|
|
107
180
|
<th
|
|
108
181
|
key={column.key}
|
|
109
182
|
className={`px-3 py-2 text-[11px] font-medium text-ink-faint ${column.className ?? ''}`}
|
|
110
183
|
>
|
|
111
|
-
{sortable ? (
|
|
184
|
+
{sortable && field !== undefined ? (
|
|
112
185
|
<button
|
|
113
186
|
type="button"
|
|
114
187
|
onClick={() => {
|
|
115
|
-
onSortChange?.(nextSort(sort,
|
|
188
|
+
onSortChange?.(nextSort(sort, field))
|
|
116
189
|
}}
|
|
117
190
|
className="inline-flex items-center gap-1 transition hover:text-ink"
|
|
118
191
|
>
|
|
@@ -130,26 +203,32 @@ export function DataTable<TRow>({
|
|
|
130
203
|
</tr>
|
|
131
204
|
</thead>
|
|
132
205
|
<tbody>
|
|
133
|
-
{
|
|
134
|
-
? (
|
|
206
|
+
{displayedGroups === undefined
|
|
207
|
+
? (displayedRows ?? []).map((row) => (
|
|
135
208
|
<DataRow
|
|
136
209
|
key={getRowId(row)}
|
|
137
210
|
row={row}
|
|
138
|
-
columns={
|
|
211
|
+
columns={displayedColumns}
|
|
139
212
|
onRowClick={onRowClick}
|
|
140
213
|
/>
|
|
141
214
|
))
|
|
142
|
-
:
|
|
215
|
+
: displayedGroups.map((group) => (
|
|
143
216
|
<GroupRows
|
|
144
217
|
key={group.id}
|
|
145
218
|
group={group}
|
|
146
|
-
columns={
|
|
219
|
+
columns={displayedColumns}
|
|
147
220
|
onRowClick={onRowClick}
|
|
148
221
|
getRowId={getRowId}
|
|
149
222
|
/>
|
|
150
223
|
))}
|
|
151
224
|
</tbody>
|
|
152
225
|
</table>
|
|
226
|
+
{clientSortColumn !== undefined && (
|
|
227
|
+
<p className="border-t border-border bg-surface-sunken/40 px-3 py-1.5 text-[11px] text-ink-faint">
|
|
228
|
+
Sorted by {clientSortColumn.header.toLowerCase()} on this page. Other pages sort by their
|
|
229
|
+
own defaults.
|
|
230
|
+
</p>
|
|
231
|
+
)}
|
|
153
232
|
</div>
|
|
154
233
|
)
|
|
155
234
|
}
|
|
@@ -7,6 +7,7 @@ import { useTimezone } from '../api/resources/account.ts'
|
|
|
7
7
|
import { useCreateDecision, useDecisions, useDeleteDecision } from '../api/resources/decisions.ts'
|
|
8
8
|
import { useMembers } from '../api/resources/members.ts'
|
|
9
9
|
import { formatDate } from '../lib/dates.ts'
|
|
10
|
+
import { Paginator } from './Paginator.tsx'
|
|
10
11
|
import { ErrorPanel } from './QueryState.tsx'
|
|
11
12
|
import { SectionHeader } from './SectionHeader.tsx'
|
|
12
13
|
|
|
@@ -35,8 +36,8 @@ export function DecisionsPanel({ targetType, targetId }: DecisionsPanelProps): R
|
|
|
35
36
|
|
|
36
37
|
// Due date first, decided date otherwise, newest down — the mockup's order.
|
|
37
38
|
// The list arrives in `-decided_at` order and the API cannot sort on the
|
|
38
|
-
// nullable `due_at`, so this reorders what is
|
|
39
|
-
//
|
|
39
|
+
// nullable `due_at`, so this reorders what is on this page; across page
|
|
40
|
+
// boundaries the interleave can be wrong, which is the cheaper wrong than a
|
|
40
41
|
// second request per panel.
|
|
41
42
|
const ordered = [...decisions.records].sort(
|
|
42
43
|
(left, right) =>
|
|
@@ -167,16 +168,7 @@ export function DecisionsPanel({ targetType, targetId }: DecisionsPanelProps): R
|
|
|
167
168
|
)
|
|
168
169
|
)}
|
|
169
170
|
|
|
170
|
-
{decisions
|
|
171
|
-
<button
|
|
172
|
-
type="button"
|
|
173
|
-
onClick={decisions.loadMore}
|
|
174
|
-
disabled={decisions.isLoadingMore}
|
|
175
|
-
className="mt-3 rounded-md border border-border px-3 py-1.5 text-[12px] font-medium text-ink-muted transition hover:border-border-strong hover:text-ink"
|
|
176
|
-
>
|
|
177
|
-
{decisions.isLoadingMore ? 'Loading…' : 'Load more'}
|
|
178
|
-
</button>
|
|
179
|
-
)}
|
|
171
|
+
<Paginator list={decisions} />
|
|
180
172
|
|
|
181
173
|
<p className="mt-2 text-[11px] text-ink-faint">
|
|
182
174
|
<Link to="/decisions" className="text-accent hover:underline">
|
|
@@ -50,10 +50,16 @@ export function EntitySearch({
|
|
|
50
50
|
}: EntitySearchProps): React.JSX.Element {
|
|
51
51
|
const listId = useId()
|
|
52
52
|
const rootRef = useRef<HTMLDivElement>(null)
|
|
53
|
-
const selected = options.find((option) => option.id === value)
|
|
54
53
|
const [query, setQuery] = useState('')
|
|
55
54
|
const [open, setOpen] = useState(false)
|
|
56
55
|
const [highlight, setHighlight] = useState(0)
|
|
56
|
+
// A picked option must keep rendering even when the current `options` list
|
|
57
|
+
// no longer contains it — for a server-backed search, clearing the query on
|
|
58
|
+
// pick swaps the answer to a different page that may not include the pick.
|
|
59
|
+
const [remembered, setRemembered] = useState<SearchOption | null>(null)
|
|
60
|
+
const selected =
|
|
61
|
+
options.find((option) => option.id === value) ??
|
|
62
|
+
(remembered !== null && remembered.id === value ? remembered : undefined)
|
|
57
63
|
|
|
58
64
|
const filtered = useMemo(() => {
|
|
59
65
|
const term = query.trim().toLowerCase()
|
|
@@ -105,8 +111,9 @@ export function EntitySearch({
|
|
|
105
111
|
onQueryChange?.(next)
|
|
106
112
|
}
|
|
107
113
|
|
|
108
|
-
function pick(
|
|
109
|
-
|
|
114
|
+
function pick(option: SearchOption): void {
|
|
115
|
+
setRemembered(option)
|
|
116
|
+
onChange(option.id)
|
|
110
117
|
setOpen(false)
|
|
111
118
|
search('')
|
|
112
119
|
}
|
|
@@ -183,7 +190,7 @@ export function EntitySearch({
|
|
|
183
190
|
const hit = filtered[highlight]
|
|
184
191
|
|
|
185
192
|
if (hit !== undefined) {
|
|
186
|
-
pick(hit
|
|
193
|
+
pick(hit)
|
|
187
194
|
} else if (showCreate) {
|
|
188
195
|
create()
|
|
189
196
|
}
|
|
@@ -219,7 +226,7 @@ export function EntitySearch({
|
|
|
219
226
|
setHighlight(index)
|
|
220
227
|
}}
|
|
221
228
|
onClick={() => {
|
|
222
|
-
pick(option
|
|
229
|
+
pick(option)
|
|
223
230
|
}}
|
|
224
231
|
className={[
|
|
225
232
|
'flex w-full flex-col px-3 py-1.5 text-left',
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { usePublicConfig } from '../api/resources/publicConfig.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A thin strip above the app that names the site.
|
|
5
|
+
*
|
|
6
|
+
* Renders nothing on production, whether or not a site name was set: the
|
|
7
|
+
* banner exists to tell dev, demo, and any other non-production install
|
|
8
|
+
* apart. Renders nothing while the metadata is loading or on error either,
|
|
9
|
+
* so a slow config request never pushes the app down half a second after
|
|
10
|
+
* boot.
|
|
11
|
+
*
|
|
12
|
+
* Mounted at the top of `KelpieApp`, above the router, so it shows on the
|
|
13
|
+
* sign-in and onboarding pages as well as the main app.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export function EnvironmentBanner(): React.JSX.Element | null {
|
|
17
|
+
const { config } = usePublicConfig()
|
|
18
|
+
|
|
19
|
+
if (config === undefined || config.runtimeMode === 'production') {
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const label = config.siteName ?? config.runtimeMode
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="flex h-5 shrink-0 items-center justify-center bg-accent px-2 text-[11px] font-medium tracking-wide text-accent-fg uppercase">
|
|
27
|
+
{label}
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { RECORD_TARGET_TYPE_LABELS } from '@kelpie/schemas'
|
|
2
|
+
import type { List, ListMembership, RecordTargetType } from '@kelpie/schemas'
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { Link } from 'react-router'
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
useAddListMember,
|
|
8
|
+
useListMembershipsFor,
|
|
9
|
+
useRemoveListMember,
|
|
10
|
+
} from '../api/resources/listMembers.ts'
|
|
11
|
+
import { useLists } from '../api/resources/lists.ts'
|
|
12
|
+
import { EntitySearch } from './EntitySearch.tsx'
|
|
13
|
+
import type { SearchOption } from './EntitySearch.tsx'
|
|
14
|
+
import { ErrorPanel, LoadingPanel } from './QueryState.tsx'
|
|
15
|
+
import { SectionHeader } from './SectionHeader.tsx'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* "What lists is this record on?" panel, for any record's detail page.
|
|
19
|
+
*
|
|
20
|
+
* A record can only join a list whose `target_type` matches its own, so the
|
|
21
|
+
* picker is filtered accordingly. Removing a chip calls
|
|
22
|
+
* `DELETE /v1/lists/{id}/members/{memberId}` and the cached counts on the
|
|
23
|
+
* lists index refresh with it.
|
|
24
|
+
*/
|
|
25
|
+
export function ListsPanel({
|
|
26
|
+
targetType,
|
|
27
|
+
targetId,
|
|
28
|
+
}: {
|
|
29
|
+
readonly targetType: RecordTargetType
|
|
30
|
+
readonly targetId: string
|
|
31
|
+
}): React.JSX.Element {
|
|
32
|
+
const memberships = useListMembershipsFor(targetType, targetId)
|
|
33
|
+
const [adding, setAdding] = useState(false)
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<section>
|
|
37
|
+
<SectionHeader
|
|
38
|
+
title="Lists"
|
|
39
|
+
onAdd={() => {
|
|
40
|
+
setAdding((current) => !current)
|
|
41
|
+
}}
|
|
42
|
+
addLabel="Add to list"
|
|
43
|
+
/>
|
|
44
|
+
|
|
45
|
+
{adding && (
|
|
46
|
+
<div className="mb-3 flex gap-2">
|
|
47
|
+
<div className="min-w-0 flex-1">
|
|
48
|
+
<ListPicker
|
|
49
|
+
targetType={targetType}
|
|
50
|
+
targetId={targetId}
|
|
51
|
+
excludeListIds={new Set(memberships.memberships.map((row) => row.listId))}
|
|
52
|
+
onAdded={() => {
|
|
53
|
+
setAdding(false)
|
|
54
|
+
}}
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
<button
|
|
58
|
+
type="button"
|
|
59
|
+
onClick={() => {
|
|
60
|
+
setAdding(false)
|
|
61
|
+
}}
|
|
62
|
+
className="shrink-0 rounded-md px-2.5 py-1.5 text-[12px] font-medium text-ink-muted hover:text-ink"
|
|
63
|
+
>
|
|
64
|
+
Cancel
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
|
|
69
|
+
{memberships.error !== null && <ErrorPanel error={memberships.error} />}
|
|
70
|
+
|
|
71
|
+
{memberships.isLoading ? (
|
|
72
|
+
<LoadingPanel label="Loading lists…" />
|
|
73
|
+
) : memberships.memberships.length === 0 && !adding ? (
|
|
74
|
+
<p className="text-[13px] text-ink-faint">
|
|
75
|
+
Not on any lists yet. Add this {RECORD_TARGET_TYPE_LABELS[targetType].toLowerCase()} to
|
|
76
|
+
a list to group it with others.
|
|
77
|
+
</p>
|
|
78
|
+
) : (
|
|
79
|
+
<ul className="overflow-hidden rounded-md border border-border">
|
|
80
|
+
{memberships.memberships.map((membership) => (
|
|
81
|
+
<MembershipRow key={membership.id} membership={membership} />
|
|
82
|
+
))}
|
|
83
|
+
</ul>
|
|
84
|
+
)}
|
|
85
|
+
</section>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function MembershipRow({
|
|
90
|
+
membership,
|
|
91
|
+
}: {
|
|
92
|
+
readonly membership: ListMembership
|
|
93
|
+
}): React.JSX.Element {
|
|
94
|
+
const remove = useRemoveListMember()
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<li className="border-b border-border px-4 py-3 last:border-0">
|
|
98
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
99
|
+
<Link
|
|
100
|
+
to={`/lists/${membership.listId}`}
|
|
101
|
+
className="text-left text-[13px] font-medium text-ink hover:text-accent"
|
|
102
|
+
>
|
|
103
|
+
{membership.listName}
|
|
104
|
+
</Link>
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
disabled={remove.isPending}
|
|
108
|
+
onClick={() => {
|
|
109
|
+
remove.run({ listId: membership.listId, id: membership.id })
|
|
110
|
+
}}
|
|
111
|
+
className="text-[11px] font-medium text-danger hover:underline disabled:opacity-50"
|
|
112
|
+
>
|
|
113
|
+
Remove
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{remove.error !== null && (
|
|
118
|
+
<div className="mt-2">
|
|
119
|
+
<ErrorPanel error={remove.error} />
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
</li>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function ListPicker({
|
|
127
|
+
targetType,
|
|
128
|
+
targetId,
|
|
129
|
+
excludeListIds,
|
|
130
|
+
onAdded,
|
|
131
|
+
}: {
|
|
132
|
+
readonly targetType: RecordTargetType
|
|
133
|
+
readonly targetId: string
|
|
134
|
+
readonly excludeListIds: ReadonlySet<string>
|
|
135
|
+
readonly onAdded: () => void
|
|
136
|
+
}): React.JSX.Element {
|
|
137
|
+
const [search, setSearch] = useState('')
|
|
138
|
+
const term = search.trim()
|
|
139
|
+
// Only lists whose type matches — the DB will refuse a mismatched member,
|
|
140
|
+
// but the picker filters up front so the surface never offers one.
|
|
141
|
+
const lists = useLists({
|
|
142
|
+
targetType,
|
|
143
|
+
term: term.length > 0 ? term : undefined,
|
|
144
|
+
})
|
|
145
|
+
const addMember = useAddListMember()
|
|
146
|
+
|
|
147
|
+
const options: readonly SearchOption[] = lists.records
|
|
148
|
+
.filter((row: List) => !excludeListIds.has(row.id))
|
|
149
|
+
.map((row: List) => ({ id: row.id, label: row.name }))
|
|
150
|
+
|
|
151
|
+
function pick(listId: string): void {
|
|
152
|
+
addMember
|
|
153
|
+
.runAsync({ listId, input: { targetType, targetId } })
|
|
154
|
+
.then(onAdded)
|
|
155
|
+
.catch(() => undefined)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<div>
|
|
160
|
+
<EntitySearch
|
|
161
|
+
options={options}
|
|
162
|
+
value=""
|
|
163
|
+
onChange={pick}
|
|
164
|
+
onQueryChange={setSearch}
|
|
165
|
+
placeholder="Search lists…"
|
|
166
|
+
emptyMessage="No matching lists"
|
|
167
|
+
size="md"
|
|
168
|
+
/>
|
|
169
|
+
{addMember.error !== null && (
|
|
170
|
+
<div className="mt-2">
|
|
171
|
+
<ErrorPanel error={addMember.error} />
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
@@ -6,6 +6,7 @@ import { useTimezone } from '../api/resources/account.ts'
|
|
|
6
6
|
import { useMembers } from '../api/resources/members.ts'
|
|
7
7
|
import { useCreateNote, useNotes } from '../api/resources/notes.ts'
|
|
8
8
|
import { formatDateTime } from '../lib/dates.ts'
|
|
9
|
+
import { Paginator } from './Paginator.tsx'
|
|
9
10
|
import { ErrorPanel } from './QueryState.tsx'
|
|
10
11
|
import { SectionHeader } from './SectionHeader.tsx'
|
|
11
12
|
|
|
@@ -33,10 +34,10 @@ export function NotesPanel({ targetType, targetId }: NotesPanelProps): React.JSX
|
|
|
33
34
|
const [body, setBody] = useState('')
|
|
34
35
|
|
|
35
36
|
// Pinned first, then newest. The list arrives in `-created_at` order and the
|
|
36
|
-
// API has no two-column sort, so this reorders what is
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
37
|
+
// API has no two-column sort, so this reorders what is on this page. Across
|
|
38
|
+
// page boundaries a pinned note on a later page sorts after unpinned ones on
|
|
39
|
+
// an earlier one; pinned notes are few enough that this is the cheaper wrong
|
|
40
|
+
// than a second request per panel.
|
|
40
41
|
const ordered = [...notes.records].sort((left, right) => Number(right.pinned) - Number(left.pinned))
|
|
41
42
|
|
|
42
43
|
function reset(): void {
|
|
@@ -116,16 +117,7 @@ export function NotesPanel({ targetType, targetId }: NotesPanelProps): React.JSX
|
|
|
116
117
|
</ul>
|
|
117
118
|
)}
|
|
118
119
|
|
|
119
|
-
{notes
|
|
120
|
-
<button
|
|
121
|
-
type="button"
|
|
122
|
-
onClick={notes.loadMore}
|
|
123
|
-
disabled={notes.isLoadingMore}
|
|
124
|
-
className="mt-3 rounded-md border border-border px-3 py-1.5 text-[12px] font-medium text-ink-muted transition hover:border-border-strong hover:text-ink"
|
|
125
|
-
>
|
|
126
|
-
{notes.isLoadingMore ? 'Loading…' : 'Load more'}
|
|
127
|
-
</button>
|
|
128
|
-
)}
|
|
120
|
+
<Paginator list={notes} />
|
|
129
121
|
</section>
|
|
130
122
|
)
|
|
131
123
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Paged } from '../api/resource.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Prev / Next with a page indicator and an optional page size selector.
|
|
5
|
+
*
|
|
6
|
+
* The API is cursor-only (`api.md`), so there is no total count and no jump
|
|
7
|
+
* to page N. `pageIndex` is one-based on screen but zero-based in `Paged` —
|
|
8
|
+
* the label adds one so page 1 does not read as page 0.
|
|
9
|
+
*
|
|
10
|
+
* `Prev` disables when nothing came before, `Next` when the server said the
|
|
11
|
+
* current page was the last one. A cached prior page is instant; going past
|
|
12
|
+
* the fetched set kicks off exactly one fetch, and both buttons hold their
|
|
13
|
+
* disabled state until it settles so a fast double click does not skip two.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface PaginatorProps {
|
|
17
|
+
readonly list: Paged
|
|
18
|
+
/** Options offered by the page size selector. Omit to hide the selector. */
|
|
19
|
+
readonly pageSizes?: readonly number[]
|
|
20
|
+
/**
|
|
21
|
+
* Where on the list this paginator sits. `top` places it above the rows —
|
|
22
|
+
* the per-page selector then defers to the `bottom` copy so the two do not
|
|
23
|
+
* duplicate the same control side by side.
|
|
24
|
+
*/
|
|
25
|
+
readonly placement?: 'top' | 'bottom'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const DEFAULT_PAGE_SIZES: readonly number[] = [25, 50, 100, 200]
|
|
29
|
+
|
|
30
|
+
const buttonClass =
|
|
31
|
+
'rounded-md border border-border bg-surface-raised px-2.5 py-1 text-[12px] font-medium text-ink transition hover:border-border-strong hover:bg-surface-sunken disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:border-border disabled:hover:bg-surface-raised'
|
|
32
|
+
|
|
33
|
+
export function Paginator({
|
|
34
|
+
list,
|
|
35
|
+
pageSizes = DEFAULT_PAGE_SIZES,
|
|
36
|
+
placement = 'bottom',
|
|
37
|
+
}: PaginatorProps): React.JSX.Element {
|
|
38
|
+
const prevDisabled = !list.hasPrev || list.isChangingPage
|
|
39
|
+
const nextDisabled = !list.hasNext || list.isChangingPage
|
|
40
|
+
const options = pageSizes.includes(list.pageSize) ? pageSizes : [...pageSizes, list.pageSize].sort((a, b) => a - b)
|
|
41
|
+
const showPageSize = placement === 'bottom' && pageSizes.length > 0
|
|
42
|
+
const wrapperClass =
|
|
43
|
+
placement === 'top'
|
|
44
|
+
? 'mt-3 mb-3 flex flex-wrap items-center gap-3 text-[12px] text-ink-muted'
|
|
45
|
+
: 'mt-3 flex flex-wrap items-center gap-3 text-[12px] text-ink-muted'
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className={wrapperClass}>
|
|
49
|
+
<div className="flex items-center gap-1">
|
|
50
|
+
<button type="button" onClick={list.prevPage} disabled={prevDisabled} className={buttonClass}>
|
|
51
|
+
Prev
|
|
52
|
+
</button>
|
|
53
|
+
<button type="button" onClick={list.nextPage} disabled={nextDisabled} className={buttonClass}>
|
|
54
|
+
{list.isChangingPage ? 'Loading…' : 'Next'}
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
<span aria-live="polite">Page {list.pageIndex + 1}</span>
|
|
58
|
+
{showPageSize && (
|
|
59
|
+
<label className="ml-auto flex items-center gap-2">
|
|
60
|
+
<span>Per page</span>
|
|
61
|
+
<select
|
|
62
|
+
value={list.pageSize}
|
|
63
|
+
onChange={(event) => {
|
|
64
|
+
list.setPageSize(Number(event.target.value))
|
|
65
|
+
}}
|
|
66
|
+
className="rounded-md border border-border bg-surface-raised px-2 py-1 text-[12px] outline-none focus:border-accent"
|
|
67
|
+
>
|
|
68
|
+
{options.map((size) => (
|
|
69
|
+
<option key={size} value={size}>
|
|
70
|
+
{size}
|
|
71
|
+
</option>
|
|
72
|
+
))}
|
|
73
|
+
</select>
|
|
74
|
+
</label>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { formatDay } from '../lib/dates.ts'
|
|
14
14
|
import { planStatusTone } from '../lib/plan.ts'
|
|
15
15
|
import { Chip } from './Chip.tsx'
|
|
16
|
+
import { Paginator } from './Paginator.tsx'
|
|
16
17
|
import { ErrorPanel } from './QueryState.tsx'
|
|
17
18
|
import { SectionHeader } from './SectionHeader.tsx'
|
|
18
19
|
|
|
@@ -102,16 +103,7 @@ export function PlanPanel({ targetType, targetId }: PlanPanelProps): React.JSX.E
|
|
|
102
103
|
</ul>
|
|
103
104
|
)}
|
|
104
105
|
|
|
105
|
-
{items
|
|
106
|
-
<button
|
|
107
|
-
type="button"
|
|
108
|
-
onClick={items.loadMore}
|
|
109
|
-
disabled={items.isLoadingMore}
|
|
110
|
-
className="mt-3 rounded-md border border-border px-3 py-1.5 text-[12px] font-medium text-ink-muted transition hover:border-border-strong hover:text-ink"
|
|
111
|
-
>
|
|
112
|
-
{items.isLoadingMore ? 'Loading…' : 'Load more'}
|
|
113
|
-
</button>
|
|
114
|
-
)}
|
|
106
|
+
<Paginator list={items} />
|
|
115
107
|
</section>
|
|
116
108
|
)
|
|
117
109
|
}
|
package/src/components/Shell.tsx
CHANGED
|
@@ -38,6 +38,7 @@ const CORE_NAV: readonly NavItem[] = [
|
|
|
38
38
|
{ id: 'opportunities', label: 'Opportunities', to: '/opportunities', order: 350 },
|
|
39
39
|
{ id: 'fundraising', label: 'Fundraising', to: '/fundraising', order: 360 },
|
|
40
40
|
{ id: 'partnerships', label: 'Partnerships', to: '/partnerships', order: 375 },
|
|
41
|
+
{ id: 'lists', label: 'Lists', to: '/lists', order: 378 },
|
|
41
42
|
{ id: 'forms', label: 'Forms', to: '/forms', order: 380 },
|
|
42
43
|
{ id: 'handbook', label: 'Handbook', to: '/handbook', order: 390 },
|
|
43
44
|
{ id: 'planning', label: 'Planning', to: '/planning', order: 400 },
|
package/src/index.ts
CHANGED
|
@@ -13,10 +13,12 @@ export type { ApiProviderProps } from './api/ApiProvider.tsx'
|
|
|
13
13
|
export { useApiClient } from './api/context.ts'
|
|
14
14
|
export { toError } from './api/errors.ts'
|
|
15
15
|
export { createQueryClient } from './api/queryClient.ts'
|
|
16
|
-
export { createResourceHooks } from './api/resource.ts'
|
|
16
|
+
export { createResourceHooks, DEFAULT_PAGE_SIZE, usePagedList } from './api/resource.ts'
|
|
17
17
|
export type {
|
|
18
18
|
ListOptions,
|
|
19
19
|
MutationResult,
|
|
20
|
+
Paged,
|
|
21
|
+
PagedListInput,
|
|
20
22
|
RecordListResult,
|
|
21
23
|
RecordResult,
|
|
22
24
|
ResourceDefinition,
|
|
@@ -88,6 +90,8 @@ export { AgentTasks } from './components/AgentTasks.tsx'
|
|
|
88
90
|
export type { AgentTasksProps } from './components/AgentTasks.tsx'
|
|
89
91
|
export { Chip } from './components/Chip.tsx'
|
|
90
92
|
export type { ChipProps, ChipTone } from './components/Chip.tsx'
|
|
93
|
+
export { ColumnPicker } from './components/ColumnPicker.tsx'
|
|
94
|
+
export type { ColumnPickerOption, ColumnPickerProps } from './components/ColumnPicker.tsx'
|
|
91
95
|
export { DataTable } from './components/DataTable.tsx'
|
|
92
96
|
export type { Column, DataTableGroup, DataTableProps } from './components/DataTable.tsx'
|
|
93
97
|
export { DecisionsPanel } from './components/DecisionsPanel.tsx'
|
|
@@ -100,6 +104,8 @@ export { InlineEdit } from './components/InlineEdit.tsx'
|
|
|
100
104
|
export type { InlineEditProps } from './components/InlineEdit.tsx'
|
|
101
105
|
export { FilterBar, PageHeader } from './components/PageHeader.tsx'
|
|
102
106
|
export type { FilterBarProps, PageHeaderProps } from './components/PageHeader.tsx'
|
|
107
|
+
export { Paginator } from './components/Paginator.tsx'
|
|
108
|
+
export type { PaginatorProps } from './components/Paginator.tsx'
|
|
103
109
|
export { PhonesField } from './components/PhonesField.tsx'
|
|
104
110
|
export type { PhonesFieldProps } from './components/PhonesField.tsx'
|
|
105
111
|
export { PlanAttention, RelatedPlanAttention } from './components/PlanAttention.tsx'
|
|
@@ -151,6 +157,9 @@ export {
|
|
|
151
157
|
} from './lib/plan.ts'
|
|
152
158
|
export type { DueBucketId, PlanAttention as PlanAttentionSummary } from './lib/plan.ts'
|
|
153
159
|
|
|
160
|
+
export { useListView } from './lib/listView.ts'
|
|
161
|
+
export type { ListViewController } from './lib/listView.ts'
|
|
162
|
+
|
|
154
163
|
export { applyTheme, getStoredTheme, resolveTheme, setStoredTheme, watchSystemTheme } from './lib/theme.ts'
|
|
155
164
|
export type { ThemePreference } from './lib/theme.ts'
|
|
156
165
|
|