@exxatdesignux/ui 0.2.17 → 0.2.19
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/CHANGELOG.md +30 -0
- package/consumer-extras/AGENTS.md +76 -0
- package/consumer-extras/README.md +5 -1
- package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +14 -3
- package/consumer-extras/cursor-skills/exxat-consumer-app/SKILL.md +37 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +22 -7
- package/consumer-extras/cursor-skills/exxat-focused-workflow-page/SKILL.md +57 -0
- package/consumer-extras/cursor-skills/exxat-kpi-flat-band/SKILL.md +38 -0
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +10 -3
- package/consumer-extras/patterns/consumer-app-pattern.md +39 -0
- package/consumer-extras/patterns/consumer-upgrade-checklist.md +20 -0
- package/consumer-extras/patterns/data-views-pattern.md +42 -3
- package/consumer-extras/patterns/focused-workflow-page-pattern.md +84 -0
- package/consumer-extras/patterns/kpi-flat-band-pattern.md +57 -0
- package/consumer-extras/patterns/shell-surface-elevation-pattern.md +54 -0
- package/package.json +2 -1
- package/src/components/ui/button-group.tsx +81 -0
- package/src/components/ui/button.tsx +4 -4
- package/src/components/ui/sidebar.tsx +2 -2
- package/src/globals.css +7 -1807
- package/src/theme.css +10 -1126
- package/src/tokens/README.md +15 -0
- package/src/tokens/base.css +337 -0
- package/src/tokens/high-contrast.css +1195 -0
- package/src/tokens/layers.css +224 -0
- package/src/tokens/tailwind-bridge.css +118 -0
- package/src/tokens/themes.css +201 -0
- package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +1 -1
- package/template/AGENTS.md +66 -21
- package/template/app/(app)/dashboard/loading.tsx +3 -15
- package/template/app/(app)/dashboard/page.tsx +2 -14
- package/template/app/(app)/data-list/layout.tsx +43 -0
- package/template/app/(app)/data-list/page.tsx +2 -2
- package/template/app/(app)/error.tsx +22 -6
- package/template/app/(app)/examples/focused-workflow/page.tsx +5 -0
- package/template/app/(app)/examples/page.tsx +1 -0
- package/template/app/(app)/layout.tsx +13 -6
- package/template/app/(app)/loading.tsx +1 -18
- package/template/app/(app)/question-bank/find/page.tsx +2 -1
- package/template/app/(app)/question-bank/library/page.tsx +2 -1
- package/template/app/(app)/question-bank/list/page.tsx +2 -1
- package/template/app/(app)/question-bank/new/page.tsx +15 -23
- package/template/app/(app)/question-bank/page.tsx +2 -1
- package/template/app/(app)/settings/page.tsx +4 -5
- package/template/app/global-error.tsx +63 -0
- package/template/app/globals.css +7 -1934
- package/template/app/layout.tsx +2 -0
- package/template/components/app-route-loading.tsx +14 -0
- package/template/components/app-sidebar.tsx +71 -55
- package/template/components/data-table/index.tsx +31 -67
- package/template/components/data-table/use-table-state.ts +33 -6
- package/template/components/data-views/index.ts +37 -9
- package/template/components/data-views/list-page-calendar-view.tsx +593 -0
- package/template/components/data-views/list-page-connected-view-body.tsx +66 -0
- package/template/components/data-views/list-page-folder-columns-panel.tsx +345 -0
- package/template/components/data-views/list-page-split-hub-chrome.tsx +8 -0
- package/template/components/dev-chunk-load-recovery.tsx +41 -0
- package/template/components/examples/focused-workflow-showcase.tsx +183 -0
- package/template/components/exxat-product-logo.tsx +2 -6
- package/template/components/key-metrics.tsx +54 -22
- package/template/components/list-hub-board-view.tsx +68 -0
- package/template/components/list-hub-client.tsx +186 -0
- package/template/components/list-hub-list-view.tsx +36 -0
- package/template/components/list-hub-panel-activator.tsx +8 -0
- package/template/components/list-hub-secondary-nav.tsx +121 -0
- package/template/components/list-hub-table.tsx +336 -0
- package/template/components/new-question-composer.tsx +6 -24
- package/template/components/product-switcher.tsx +5 -5
- package/template/components/product-wordmark.tsx +4 -7
- package/template/components/question-bank-client.tsx +4 -1
- package/template/components/question-bank-folder-columns-panel.tsx +104 -0
- package/template/components/question-bank-hub-client.tsx +2 -5
- package/template/components/question-bank-table.tsx +155 -509
- package/template/components/secondary-panel/nav-link-rows.tsx +83 -0
- package/template/components/secondary-panel.tsx +4 -44
- package/template/components/secondary-panels/list-hub-panel.tsx +39 -0
- package/template/components/secondary-panels/question-bank-panel.tsx +39 -0
- package/template/components/secondary-panels/registry.tsx +15 -0
- package/template/components/settings-appearance-card.tsx +3 -2
- package/template/components/settings-client.tsx +59 -15
- package/template/components/settings-form-row.tsx +9 -4
- package/template/components/sidebar-shell.tsx +2 -1
- package/template/components/table-properties/drawer-button.tsx +51 -20
- package/template/components/table-properties/drawer.tsx +81 -17
- package/template/components/templates/focused-workflow-layouts.tsx +448 -0
- package/template/components/templates/focused-workflow-page-template.tsx +69 -0
- package/template/components/templates/list-page.tsx +40 -13
- package/template/components/templates/nested-secondary-panel-shell.tsx +3 -2
- package/template/components/templates/page-loading-shell.tsx +262 -0
- package/template/components/ui/button-group.tsx +1 -0
- package/template/contexts/product-context.tsx +21 -2
- package/template/docs/consumer-app-pattern.md +39 -0
- package/template/docs/data-views-pattern.md +42 -3
- package/template/docs/drawer-vs-dialog-pattern.md +3 -1
- package/template/docs/focused-workflow-page-pattern.md +84 -0
- package/template/docs/kpi-flat-band-pattern.md +57 -0
- package/template/docs/kpi-strip-max-four-pattern.md +1 -0
- package/template/docs/shell-surface-elevation-pattern.md +54 -0
- package/template/lib/chunk-load-error.ts +13 -0
- package/template/lib/command-menu-search-data.ts +11 -27
- package/template/lib/conditional-rule-match.ts +87 -22
- package/template/lib/data-list-display-options.ts +16 -2
- package/template/lib/data-list-view-registry.ts +104 -0
- package/template/lib/data-list-view-surface.ts +15 -1
- package/template/lib/data-list-view.ts +16 -1
- package/template/lib/data-view-dashboard-storage.ts +38 -35
- package/template/lib/hub-connected-view-renderers.ts +58 -0
- package/template/lib/list-hub-nav.ts +121 -0
- package/template/lib/list-hub-supported-views.ts +10 -0
- package/template/lib/list-page-table-properties.ts +3 -7
- package/template/lib/list-status-badges.ts +4 -97
- package/template/lib/mock/list-hub-directory.ts +27 -0
- package/template/lib/mock/list-hub-kpi.ts +27 -0
- package/template/lib/mock/navigation.tsx +1 -0
- package/template/lib/page-loading-variant.ts +40 -0
- package/template/lib/question-bank-supported-views.ts +13 -0
- package/template/lib/sidebar-state-cookie.ts +9 -0
- package/template/lib/table-state-lifecycle.ts +60 -13
- package/template/app/(app)/data-list/[id]/page.tsx +0 -44
- package/template/app/(app)/data-list/new/page.tsx +0 -34
- package/template/components/compliance-board-view.tsx +0 -142
- package/template/components/compliance-client.tsx +0 -92
- package/template/components/compliance-list-view.tsx +0 -54
- package/template/components/compliance-page-header.tsx +0 -89
- package/template/components/compliance-table.tsx +0 -632
- package/template/components/data-view-dashboard-charts-compliance.tsx +0 -963
- package/template/components/data-view-dashboard-charts-team.tsx +0 -971
- package/template/components/data-view-dashboard-charts.tsx +0 -1503
- package/template/components/new-placement-back-btn.tsx +0 -28
- package/template/components/new-placement-form.tsx +0 -1068
- package/template/components/placement-board-card.tsx +0 -262
- package/template/components/placement-detail.tsx +0 -438
- package/template/components/placements-board-view.tsx +0 -404
- package/template/components/placements-client.tsx +0 -252
- package/template/components/placements-list-view.tsx +0 -171
- package/template/components/placements-page-header.tsx +0 -166
- package/template/components/placements-table-cells.test.tsx +0 -22
- package/template/components/placements-table-cells.tsx +0 -173
- package/template/components/placements-table-columns.tsx +0 -640
- package/template/components/placements-table.tsx +0 -1675
- package/template/components/rotations-empty-state.tsx +0 -50
- package/template/components/rotations-panel-activator.tsx +0 -8
- package/template/components/sites-all-client.tsx +0 -154
- package/template/components/sites-board-view.tsx +0 -67
- package/template/components/sites-list-view.tsx +0 -42
- package/template/components/sites-table.tsx +0 -402
- package/template/components/team-board-view.tsx +0 -122
- package/template/components/team-client.tsx +0 -100
- package/template/components/team-list-view.tsx +0 -59
- package/template/components/team-page-header.tsx +0 -92
- package/template/components/team-table.tsx +0 -714
- package/template/lib/data-view-dashboard-placements-layout.ts +0 -215
- package/template/lib/mock/compliance-kpi.ts +0 -61
- package/template/lib/mock/compliance.ts +0 -146
- package/template/lib/mock/placements-kpi.ts +0 -134
- package/template/lib/mock/placements.ts +0 -183
- package/template/lib/mock/sites-directory.ts +0 -16
- package/template/lib/mock/sites-kpi.ts +0 -25
- package/template/lib/mock/team-kpi.ts +0 -60
- package/template/lib/mock/team.ts +0 -118
- package/template/lib/placement-board-card-layout.ts +0 -79
- package/template/lib/placement-lifecycle.ts +0 -5
|
@@ -1,1675 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* PlacementsTable — placements hub composition on top of the generic
|
|
5
|
-
* `DataTable`. Owns: placement-specific column defs, board column grouping,
|
|
6
|
-
* KPI dashboards, the "open table properties" imperative handle, and the
|
|
7
|
-
* lifecycle persistence wiring (via `useTableStateLifecycle`).
|
|
8
|
-
*
|
|
9
|
-
* NOTE: this is hub composition, NOT a parallel table primitive. Every hub
|
|
10
|
-
* has its own `*-table.tsx` of the same shape (`team-table.tsx`,
|
|
11
|
-
* `compliance-table.tsx`, …); all of them render `<DataTable>` from
|
|
12
|
-
* `@/components/data-table`.
|
|
13
|
-
*
|
|
14
|
-
* View tabs drive `view` (table | list | board | …). `lifecycleTabId` selects which **demo row
|
|
15
|
-
* segment** (columns + filtered rows) to use — keep in sync with each tab's `filterId`, or pass
|
|
16
|
-
* `"all"` for tabs that only change layout.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import * as React from "react"
|
|
20
|
-
import dynamic from "next/dynamic"
|
|
21
|
-
import { cn } from "@/lib/utils"
|
|
22
|
-
import { mailtoHref } from "@/lib/mailto"
|
|
23
|
-
import { useRouter } from "next/navigation"
|
|
24
|
-
import { Button } from "@/components/ui/button"
|
|
25
|
-
import { Tip } from "@/components/ui/tip"
|
|
26
|
-
import { Skeleton } from "@/components/ui/skeleton"
|
|
27
|
-
import { KEY_METRICS_KPI_COUNT_DEFAULT } from "@/lib/dashboard-layout-merge"
|
|
28
|
-
import {
|
|
29
|
-
ALL_DASHBOARD_CARDS,
|
|
30
|
-
DEFAULT_VISIBLE_CARDS,
|
|
31
|
-
DEFAULT_SPANS,
|
|
32
|
-
DEFAULT_CHART_TYPES,
|
|
33
|
-
loadDashboardLayout,
|
|
34
|
-
mergeDashboardLayout,
|
|
35
|
-
saveDashboardLayout,
|
|
36
|
-
type ChartType,
|
|
37
|
-
type DashboardLayout,
|
|
38
|
-
} from "@/lib/data-view-dashboard-placements-layout"
|
|
39
|
-
import { CoachMark } from "@/components/ui/coach-mark"
|
|
40
|
-
import { useCoachMark } from "@/hooks/use-coach-mark"
|
|
41
|
-
import { DASHBOARD_CUSTOMIZE_COACH_STEPS } from "@/lib/dashboard-customize-coach-mark"
|
|
42
|
-
import { PlacementsBoardView, type PlacementsBoardColumnMenu } from "@/components/placements-board-view"
|
|
43
|
-
import { PlacementsListView } from "@/components/placements-list-view"
|
|
44
|
-
import { FolderGridView, ListPageTreePanelShell } from "@/components/data-views"
|
|
45
|
-
import { ListPageSplitHubChrome } from "@/components/data-views/list-page-split-hub-chrome"
|
|
46
|
-
import { ListPageTreeColumnHeader } from "@/components/data-views/list-page-tree-column-header"
|
|
47
|
-
import { FinderPanelView, type FinderGroup } from "@/components/data-views/finder-panel-view"
|
|
48
|
-
import { ListPageSplitDetailsPlaceholder } from "@/components/data-views/list-page-split-details-placeholder"
|
|
49
|
-
import { AvatarInitials } from "@/components/ui/avatar"
|
|
50
|
-
import { getConditionalRowBackground } from "@/lib/conditional-rule-match"
|
|
51
|
-
import { isBoardFieldActive } from "@/lib/placement-board-card-layout"
|
|
52
|
-
import type { BoardCardLifecycleTabId } from "@/lib/placement-board-card-layout"
|
|
53
|
-
import { TablePropertiesDrawerButton } from "@/components/table-properties"
|
|
54
|
-
import type { FilterFieldDef } from "@/components/table-properties/types"
|
|
55
|
-
import type { DataListViewType } from "@/lib/data-list-view"
|
|
56
|
-
import {
|
|
57
|
-
DEFAULT_DATA_LIST_DISPLAY_OPTIONS,
|
|
58
|
-
type DataListDisplayOptions,
|
|
59
|
-
} from "@/lib/data-list-display-options"
|
|
60
|
-
import { useTableStateLifecycle } from "@/lib/table-state-lifecycle"
|
|
61
|
-
import type { PlacementsLifecycleExtras } from "@/lib/data-list-persistence"
|
|
62
|
-
|
|
63
|
-
/** Storage namespace for the placements hub. Keep `"data-list"` so existing
|
|
64
|
-
* user payloads in localStorage remain readable. */
|
|
65
|
-
const PLACEMENTS_LIFECYCLE_NAMESPACE = "data-list"
|
|
66
|
-
import type { OpenTablePropertiesHandle } from "@/lib/list-page-table-properties"
|
|
67
|
-
import { StatusBadge } from "@/components/placements-table-cells"
|
|
68
|
-
import { columnsToFilterFields } from "@/components/placements-table-columns"
|
|
69
|
-
import { DataTable, DataTableToolbar } from "@/components/data-table"
|
|
70
|
-
import { CountSyncer, PaginationBar } from "@/components/data-table/pagination"
|
|
71
|
-
import type { DataTableExtendedProps } from "@/components/data-table"
|
|
72
|
-
import type { ColumnDef, ConditionalRule } from "@/components/data-table/types"
|
|
73
|
-
import { useTableState } from "@/components/data-table/use-table-state"
|
|
74
|
-
import { placementsForPhase, type Placement, type Status } from "@/lib/mock/placements"
|
|
75
|
-
import type { PlacementLifecycleTabId } from "@/lib/placement-lifecycle"
|
|
76
|
-
import { placementKpiInsightFromRows, placementKpiMetricsFromRows } from "@/lib/mock/placements-kpi"
|
|
77
|
-
|
|
78
|
-
const PlacementsDashboardChartsSection = dynamic(
|
|
79
|
-
() =>
|
|
80
|
-
import("@/components/data-view-dashboard-charts").then(mod => ({
|
|
81
|
-
default: mod.PlacementsDashboardChartsSection,
|
|
82
|
-
})),
|
|
83
|
-
{
|
|
84
|
-
ssr: false,
|
|
85
|
-
loading: () => (
|
|
86
|
-
<div className="mx-4 mb-8 mt-2 flex flex-col gap-3 border border-border rounded-xl p-6 lg:mx-6">
|
|
87
|
-
<Skeleton className="h-7 w-48 max-w-full" />
|
|
88
|
-
<Skeleton className="min-h-[200px] w-full rounded-lg" />
|
|
89
|
-
<Skeleton className="min-h-[200px] w-full rounded-lg" />
|
|
90
|
-
</div>
|
|
91
|
-
),
|
|
92
|
-
},
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
function DataListBoardShell({
|
|
96
|
-
state,
|
|
97
|
-
openDrawerRef,
|
|
98
|
-
tableData,
|
|
99
|
-
columns,
|
|
100
|
-
lifecycleTabId,
|
|
101
|
-
view,
|
|
102
|
-
onViewChange,
|
|
103
|
-
pagination,
|
|
104
|
-
onPaginationChange,
|
|
105
|
-
conditionalRules,
|
|
106
|
-
onAddConditionalRule,
|
|
107
|
-
onRemoveConditionalRule,
|
|
108
|
-
onUpdateConditionalRule,
|
|
109
|
-
filterFields,
|
|
110
|
-
lifecycleDrawerLabel,
|
|
111
|
-
fieldDefinitionsForDrawer,
|
|
112
|
-
resolveColumnLabel,
|
|
113
|
-
renderFilterOptionValue,
|
|
114
|
-
displayOptions,
|
|
115
|
-
onDisplayOptionsChange,
|
|
116
|
-
}: {
|
|
117
|
-
state: ReturnType<typeof useTableState<Placement>>
|
|
118
|
-
openDrawerRef: React.MutableRefObject<() => void>
|
|
119
|
-
tableData: Placement[]
|
|
120
|
-
columns: ColumnDef<Placement>[]
|
|
121
|
-
lifecycleTabId: PlacementLifecycleTabId
|
|
122
|
-
view: DataListViewType
|
|
123
|
-
onViewChange?: (view: DataListViewType) => void
|
|
124
|
-
pagination: boolean
|
|
125
|
-
onPaginationChange: (v: boolean) => void
|
|
126
|
-
conditionalRules: ConditionalRule[]
|
|
127
|
-
onAddConditionalRule: (rule: Omit<ConditionalRule, "id">) => void
|
|
128
|
-
onRemoveConditionalRule: (id: string) => void
|
|
129
|
-
onUpdateConditionalRule: (id: string, patch: Partial<ConditionalRule>) => void
|
|
130
|
-
filterFields: FilterFieldDef[]
|
|
131
|
-
lifecycleDrawerLabel: string
|
|
132
|
-
fieldDefinitionsForDrawer: { key: string; label: string; sortable?: boolean }[]
|
|
133
|
-
resolveColumnLabel: (key: string) => string
|
|
134
|
-
renderFilterOptionValue: (fieldKey: string, value: string) => React.ReactNode
|
|
135
|
-
displayOptions: DataListDisplayOptions
|
|
136
|
-
onDisplayOptionsChange: (patch: Partial<DataListDisplayOptions>) => void
|
|
137
|
-
}) {
|
|
138
|
-
// Store the "open properties drawer" callback on a stable ref so the parent
|
|
139
|
-
// imperative handle can invoke it without re-rendering the whole table.
|
|
140
|
-
// `state` is freshly returned each render by useTableState; only the React
|
|
141
|
-
// setter is stable and needed here.
|
|
142
|
-
React.useEffect(() => {
|
|
143
|
-
openDrawerRef.current = () => state.setSheetOpen(true)
|
|
144
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
145
|
-
}, [openDrawerRef, state.setSheetOpen])
|
|
146
|
-
|
|
147
|
-
const boardColumnMenu: PlacementsBoardColumnMenu = React.useMemo(
|
|
148
|
-
() => ({
|
|
149
|
-
filterableColumns: columns.filter(c => c.filter).map(c => ({ key: c.key, label: c.label })),
|
|
150
|
-
sortableColumns: columns.filter(c => c.sortable && c.sortKey).map(c => ({ key: c.key, label: c.label })),
|
|
151
|
-
groupableColumns: columns.filter(c => c.key !== "select" && c.key !== "actions").map(c => ({ key: c.key, label: c.label })),
|
|
152
|
-
groupBy: state.groupBy,
|
|
153
|
-
onAddFilter: state.addFilter,
|
|
154
|
-
onSortByField: (fieldKey, direction) => {
|
|
155
|
-
state.setSortRules(prev => {
|
|
156
|
-
const filtered = prev.filter(r => r.fieldKey !== fieldKey)
|
|
157
|
-
return [{ id: `sort-${Date.now()}`, fieldKey, direction }, ...filtered]
|
|
158
|
-
})
|
|
159
|
-
},
|
|
160
|
-
onToggleGroupBy: (fieldKey: string) => {
|
|
161
|
-
state.setGroupBy(prev => (prev === fieldKey ? null : fieldKey))
|
|
162
|
-
},
|
|
163
|
-
onOpenProperties: () => state.setSheetOpen(true),
|
|
164
|
-
}),
|
|
165
|
-
[columns, state],
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
return (
|
|
169
|
-
<>
|
|
170
|
-
<DataTableToolbar
|
|
171
|
-
state={state}
|
|
172
|
-
columns={columns}
|
|
173
|
-
searchable
|
|
174
|
-
renderFilterOptionValue={renderFilterOptionValue}
|
|
175
|
-
searchAriaLabel="Search rows"
|
|
176
|
-
toolbarSlot={(s) => (
|
|
177
|
-
<TablePropertiesDrawerButton
|
|
178
|
-
state={s}
|
|
179
|
-
totalRows={tableData.length}
|
|
180
|
-
pagination={pagination}
|
|
181
|
-
onPaginationChange={onPaginationChange}
|
|
182
|
-
conditionalRules={conditionalRules}
|
|
183
|
-
onAddConditionalRule={onAddConditionalRule}
|
|
184
|
-
onRemoveConditionalRule={onRemoveConditionalRule}
|
|
185
|
-
onUpdateConditionalRule={onUpdateConditionalRule}
|
|
186
|
-
filterFields={filterFields}
|
|
187
|
-
currentView={view}
|
|
188
|
-
onViewChange={onViewChange}
|
|
189
|
-
lifecycleTabLabel={lifecycleDrawerLabel}
|
|
190
|
-
fieldDefinitions={fieldDefinitionsForDrawer}
|
|
191
|
-
resolveColumnLabel={resolveColumnLabel}
|
|
192
|
-
displayOptions={displayOptions}
|
|
193
|
-
onDisplayOptionsChange={onDisplayOptionsChange}
|
|
194
|
-
renderFilterOptionValue={renderFilterOptionValue}
|
|
195
|
-
/>
|
|
196
|
-
)}
|
|
197
|
-
/>
|
|
198
|
-
<PlacementsBoardView
|
|
199
|
-
placements={state.rows as Placement[]}
|
|
200
|
-
lifecycleTabId={lifecycleTabId}
|
|
201
|
-
boardColumnMenu={boardColumnMenu}
|
|
202
|
-
boardDisplay={{
|
|
203
|
-
lineCount: displayOptions.boardLineCount,
|
|
204
|
-
showColumnLabels: displayOptions.showColumnLabels,
|
|
205
|
-
showColumnCounts: displayOptions.showBoardColumnCounts,
|
|
206
|
-
newCardAbove: displayOptions.boardNewCardAbove,
|
|
207
|
-
}}
|
|
208
|
-
hiddenColKeys={state.hiddenCols}
|
|
209
|
-
conditionalRules={conditionalRules}
|
|
210
|
-
boardColumns={state.displayCols.filter(c => c.key !== "select" && c.key !== "actions")}
|
|
211
|
-
/>
|
|
212
|
-
</>
|
|
213
|
-
)
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/** List / row view: shared table state + toolbar + full-width rows */
|
|
217
|
-
function DataListListShell({
|
|
218
|
-
state,
|
|
219
|
-
openDrawerRef,
|
|
220
|
-
tableData,
|
|
221
|
-
columns,
|
|
222
|
-
lifecycleTabId,
|
|
223
|
-
view,
|
|
224
|
-
onViewChange,
|
|
225
|
-
pagination,
|
|
226
|
-
onPaginationChange,
|
|
227
|
-
conditionalRules,
|
|
228
|
-
onAddConditionalRule,
|
|
229
|
-
onRemoveConditionalRule,
|
|
230
|
-
onUpdateConditionalRule,
|
|
231
|
-
filterFields,
|
|
232
|
-
lifecycleDrawerLabel,
|
|
233
|
-
fieldDefinitionsForDrawer,
|
|
234
|
-
resolveColumnLabel,
|
|
235
|
-
renderFilterOptionValue,
|
|
236
|
-
displayOptions,
|
|
237
|
-
onDisplayOptionsChange,
|
|
238
|
-
listRows,
|
|
239
|
-
emptyTableCopy,
|
|
240
|
-
}: {
|
|
241
|
-
state: ReturnType<typeof useTableState<Placement>>
|
|
242
|
-
openDrawerRef: React.MutableRefObject<() => void>
|
|
243
|
-
tableData: Placement[]
|
|
244
|
-
columns: ColumnDef<Placement>[]
|
|
245
|
-
lifecycleTabId: PlacementLifecycleTabId
|
|
246
|
-
view: DataListViewType
|
|
247
|
-
onViewChange?: (view: DataListViewType) => void
|
|
248
|
-
pagination: boolean
|
|
249
|
-
onPaginationChange: (v: boolean) => void
|
|
250
|
-
conditionalRules: ConditionalRule[]
|
|
251
|
-
onAddConditionalRule: (rule: Omit<ConditionalRule, "id">) => void
|
|
252
|
-
onRemoveConditionalRule: (id: string) => void
|
|
253
|
-
onUpdateConditionalRule: (id: string, patch: Partial<ConditionalRule>) => void
|
|
254
|
-
filterFields: FilterFieldDef[]
|
|
255
|
-
lifecycleDrawerLabel: string
|
|
256
|
-
fieldDefinitionsForDrawer: { key: string; label: string; sortable?: boolean }[]
|
|
257
|
-
resolveColumnLabel: (key: string) => string
|
|
258
|
-
renderFilterOptionValue: (fieldKey: string, value: string) => React.ReactNode
|
|
259
|
-
displayOptions: DataListDisplayOptions
|
|
260
|
-
onDisplayOptionsChange: (patch: Partial<DataListDisplayOptions>) => void
|
|
261
|
-
listRows: Placement[]
|
|
262
|
-
emptyTableCopy: string
|
|
263
|
-
}) {
|
|
264
|
-
// Stable "open properties drawer" callback ref — see top of this file.
|
|
265
|
-
React.useEffect(() => {
|
|
266
|
-
openDrawerRef.current = () => state.setSheetOpen(true)
|
|
267
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
268
|
-
}, [openDrawerRef, state.setSheetOpen])
|
|
269
|
-
|
|
270
|
-
return (
|
|
271
|
-
<>
|
|
272
|
-
<DataTableToolbar
|
|
273
|
-
state={state}
|
|
274
|
-
columns={columns}
|
|
275
|
-
searchable
|
|
276
|
-
renderFilterOptionValue={renderFilterOptionValue}
|
|
277
|
-
searchAriaLabel="Search rows"
|
|
278
|
-
toolbarSlot={s => (
|
|
279
|
-
<TablePropertiesDrawerButton
|
|
280
|
-
state={s}
|
|
281
|
-
totalRows={tableData.length}
|
|
282
|
-
pagination={pagination}
|
|
283
|
-
onPaginationChange={onPaginationChange}
|
|
284
|
-
conditionalRules={conditionalRules}
|
|
285
|
-
onAddConditionalRule={onAddConditionalRule}
|
|
286
|
-
onRemoveConditionalRule={onRemoveConditionalRule}
|
|
287
|
-
onUpdateConditionalRule={onUpdateConditionalRule}
|
|
288
|
-
filterFields={filterFields}
|
|
289
|
-
currentView={view}
|
|
290
|
-
onViewChange={onViewChange}
|
|
291
|
-
lifecycleTabLabel={lifecycleDrawerLabel}
|
|
292
|
-
fieldDefinitions={fieldDefinitionsForDrawer}
|
|
293
|
-
resolveColumnLabel={resolveColumnLabel}
|
|
294
|
-
displayOptions={displayOptions}
|
|
295
|
-
onDisplayOptionsChange={onDisplayOptionsChange}
|
|
296
|
-
renderFilterOptionValue={renderFilterOptionValue}
|
|
297
|
-
/>
|
|
298
|
-
)}
|
|
299
|
-
/>
|
|
300
|
-
<PlacementsListView
|
|
301
|
-
rows={listRows}
|
|
302
|
-
lifecycleTabId={lifecycleTabId}
|
|
303
|
-
hiddenColKeys={state.hiddenCols}
|
|
304
|
-
boardColumns={state.displayCols.filter(c => c.key !== "select" && c.key !== "actions")}
|
|
305
|
-
conditionalRules={conditionalRules}
|
|
306
|
-
emptyCopy={emptyTableCopy}
|
|
307
|
-
/>
|
|
308
|
-
</>
|
|
309
|
-
)
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/** Dashboard view tab: same toolbar + properties as list/board; KPIs from filtered rows. */
|
|
313
|
-
function DataListDashboardShell({
|
|
314
|
-
state,
|
|
315
|
-
openDrawerRef,
|
|
316
|
-
tableData,
|
|
317
|
-
columns,
|
|
318
|
-
view,
|
|
319
|
-
onViewChange,
|
|
320
|
-
pagination,
|
|
321
|
-
onPaginationChange,
|
|
322
|
-
conditionalRules,
|
|
323
|
-
onAddConditionalRule,
|
|
324
|
-
onRemoveConditionalRule,
|
|
325
|
-
onUpdateConditionalRule,
|
|
326
|
-
filterFields,
|
|
327
|
-
lifecycleDrawerLabel,
|
|
328
|
-
fieldDefinitionsForDrawer,
|
|
329
|
-
resolveColumnLabel,
|
|
330
|
-
renderFilterOptionValue,
|
|
331
|
-
displayOptions,
|
|
332
|
-
onDisplayOptionsChange,
|
|
333
|
-
}: {
|
|
334
|
-
state: ReturnType<typeof useTableState<Placement>>
|
|
335
|
-
openDrawerRef: React.MutableRefObject<() => void>
|
|
336
|
-
tableData: Placement[]
|
|
337
|
-
columns: ColumnDef<Placement>[]
|
|
338
|
-
view: DataListViewType
|
|
339
|
-
onViewChange?: (view: DataListViewType) => void
|
|
340
|
-
pagination: boolean
|
|
341
|
-
onPaginationChange: (v: boolean) => void
|
|
342
|
-
conditionalRules: ConditionalRule[]
|
|
343
|
-
onAddConditionalRule: (rule: Omit<ConditionalRule, "id">) => void
|
|
344
|
-
onRemoveConditionalRule: (id: string) => void
|
|
345
|
-
onUpdateConditionalRule: (id: string, patch: Partial<ConditionalRule>) => void
|
|
346
|
-
filterFields: FilterFieldDef[]
|
|
347
|
-
lifecycleDrawerLabel: string
|
|
348
|
-
fieldDefinitionsForDrawer: { key: string; label: string; sortable?: boolean }[]
|
|
349
|
-
resolveColumnLabel: (key: string) => string
|
|
350
|
-
renderFilterOptionValue: (fieldKey: string, value: string) => React.ReactNode
|
|
351
|
-
displayOptions: DataListDisplayOptions
|
|
352
|
-
onDisplayOptionsChange: (patch: Partial<DataListDisplayOptions>) => void
|
|
353
|
-
}) {
|
|
354
|
-
// Stable "open properties drawer" callback ref — see top of this file.
|
|
355
|
-
React.useEffect(() => {
|
|
356
|
-
openDrawerRef.current = () => state.setSheetOpen(true)
|
|
357
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
358
|
-
}, [openDrawerRef, state.setSheetOpen])
|
|
359
|
-
|
|
360
|
-
const dashboardKpi = React.useMemo(
|
|
361
|
-
() => ({
|
|
362
|
-
metrics: placementKpiMetricsFromRows(state.rows as Placement[]),
|
|
363
|
-
insight: placementKpiInsightFromRows(state.rows as Placement[]),
|
|
364
|
-
}),
|
|
365
|
-
[state.rows],
|
|
366
|
-
)
|
|
367
|
-
|
|
368
|
-
/* Dashboard card layout — persisted to localStorage */
|
|
369
|
-
const [visibleCards, setVisibleCards] = React.useState<string[]>(DEFAULT_VISIBLE_CARDS)
|
|
370
|
-
const [cardOrder, setCardOrder] = React.useState<string[]>(ALL_DASHBOARD_CARDS.map(c => c.id))
|
|
371
|
-
const [cardSpans, setCardSpans] = React.useState<Record<string, 1 | 2>>(() => ({ ...DEFAULT_SPANS }))
|
|
372
|
-
const [cardChartTypes, setCardChartTypes] = React.useState<Record<string, ChartType>>(() => ({ ...DEFAULT_CHART_TYPES }))
|
|
373
|
-
const [keyMetricsKpiCount, setKeyMetricsKpiCount] = React.useState<number>(KEY_METRICS_KPI_COUNT_DEFAULT)
|
|
374
|
-
const [dashboardLayoutEdit, setDashboardLayoutEdit] = React.useState(false)
|
|
375
|
-
const dashboardLayoutHydrated = React.useRef(false)
|
|
376
|
-
const dashboardLayoutEditBaselineRef = React.useRef<DashboardLayout | null>(null)
|
|
377
|
-
|
|
378
|
-
React.useEffect(() => {
|
|
379
|
-
const saved = loadDashboardLayout()
|
|
380
|
-
const m = mergeDashboardLayout(saved)
|
|
381
|
-
setVisibleCards(m.visible)
|
|
382
|
-
setCardOrder(m.order)
|
|
383
|
-
setCardSpans(m.spans ?? { ...DEFAULT_SPANS })
|
|
384
|
-
setCardChartTypes(m.chartTypes ?? { ...DEFAULT_CHART_TYPES })
|
|
385
|
-
setKeyMetricsKpiCount(m.keyMetricsKpiCount ?? KEY_METRICS_KPI_COUNT_DEFAULT)
|
|
386
|
-
dashboardLayoutHydrated.current = true
|
|
387
|
-
}, [])
|
|
388
|
-
|
|
389
|
-
React.useEffect(() => {
|
|
390
|
-
if (!dashboardLayoutHydrated.current) return
|
|
391
|
-
saveDashboardLayout({
|
|
392
|
-
visible: visibleCards,
|
|
393
|
-
order: cardOrder,
|
|
394
|
-
spans: cardSpans,
|
|
395
|
-
chartTypes: cardChartTypes,
|
|
396
|
-
keyMetricsKpiCount,
|
|
397
|
-
})
|
|
398
|
-
}, [visibleCards, cardOrder, cardSpans, cardChartTypes, keyMetricsKpiCount])
|
|
399
|
-
|
|
400
|
-
const handleVisibleChange = React.useCallback((v: string[]) => {
|
|
401
|
-
setVisibleCards(v)
|
|
402
|
-
}, [])
|
|
403
|
-
|
|
404
|
-
const handleOrderChange = React.useCallback((o: string[]) => {
|
|
405
|
-
setCardOrder(o)
|
|
406
|
-
}, [])
|
|
407
|
-
|
|
408
|
-
const handleSpanChange = React.useCallback((id: string, span: 1 | 2) => {
|
|
409
|
-
setCardSpans(prev => ({ ...prev, [id]: span }))
|
|
410
|
-
}, [])
|
|
411
|
-
|
|
412
|
-
const handleChartTypeChange = React.useCallback((id: string, t: ChartType) => {
|
|
413
|
-
setCardChartTypes(prev => ({ ...prev, [id]: t }))
|
|
414
|
-
}, [])
|
|
415
|
-
|
|
416
|
-
const handleResetDashboardLayout = React.useCallback(() => {
|
|
417
|
-
setVisibleCards(ALL_DASHBOARD_CARDS.map(c => c.id))
|
|
418
|
-
setCardOrder(ALL_DASHBOARD_CARDS.map(c => c.id))
|
|
419
|
-
setCardSpans({ ...DEFAULT_SPANS })
|
|
420
|
-
setCardChartTypes({ ...DEFAULT_CHART_TYPES })
|
|
421
|
-
setKeyMetricsKpiCount(KEY_METRICS_KPI_COUNT_DEFAULT)
|
|
422
|
-
}, [])
|
|
423
|
-
|
|
424
|
-
const handleDashboardLayoutEditStart = React.useCallback(() => {
|
|
425
|
-
dashboardLayoutEditBaselineRef.current = {
|
|
426
|
-
visible: [...visibleCards],
|
|
427
|
-
order: [...cardOrder],
|
|
428
|
-
spans: { ...cardSpans },
|
|
429
|
-
chartTypes: { ...cardChartTypes },
|
|
430
|
-
keyMetricsKpiCount,
|
|
431
|
-
}
|
|
432
|
-
setDashboardLayoutEdit(true)
|
|
433
|
-
}, [visibleCards, cardOrder, cardSpans, cardChartTypes, keyMetricsKpiCount])
|
|
434
|
-
|
|
435
|
-
const handleDashboardLayoutEditDone = React.useCallback(() => {
|
|
436
|
-
setDashboardLayoutEdit(false)
|
|
437
|
-
}, [])
|
|
438
|
-
|
|
439
|
-
const handleDashboardLayoutEditCancel = React.useCallback(() => {
|
|
440
|
-
const b = dashboardLayoutEditBaselineRef.current
|
|
441
|
-
if (b) {
|
|
442
|
-
setVisibleCards(b.visible)
|
|
443
|
-
setCardOrder(b.order)
|
|
444
|
-
setCardSpans(b.spans ?? { ...DEFAULT_SPANS })
|
|
445
|
-
setCardChartTypes(b.chartTypes ?? { ...DEFAULT_CHART_TYPES })
|
|
446
|
-
setKeyMetricsKpiCount(b.keyMetricsKpiCount ?? KEY_METRICS_KPI_COUNT_DEFAULT)
|
|
447
|
-
}
|
|
448
|
-
setDashboardLayoutEdit(false)
|
|
449
|
-
}, [])
|
|
450
|
-
|
|
451
|
-
const dashboardCustomizeCoach = useCoachMark({
|
|
452
|
-
flowId: "data-list-dashboard-customize",
|
|
453
|
-
steps: DASHBOARD_CUSTOMIZE_COACH_STEPS,
|
|
454
|
-
delay: 700,
|
|
455
|
-
dependsOnDismissedFlowId: "data-list-views-tour",
|
|
456
|
-
})
|
|
457
|
-
|
|
458
|
-
return (
|
|
459
|
-
<>
|
|
460
|
-
<CoachMark state={dashboardCustomizeCoach} />
|
|
461
|
-
{!dashboardLayoutEdit ? (
|
|
462
|
-
<DataTableToolbar
|
|
463
|
-
state={state}
|
|
464
|
-
columns={columns}
|
|
465
|
-
searchable={displayOptions.showToolbarSearch}
|
|
466
|
-
renderFilterOptionValue={renderFilterOptionValue}
|
|
467
|
-
searchAriaLabel="Search rows"
|
|
468
|
-
toolbarSlot={s => (
|
|
469
|
-
<TablePropertiesDrawerButton
|
|
470
|
-
state={s}
|
|
471
|
-
totalRows={tableData.length}
|
|
472
|
-
pagination={pagination}
|
|
473
|
-
onPaginationChange={onPaginationChange}
|
|
474
|
-
conditionalRules={conditionalRules}
|
|
475
|
-
onAddConditionalRule={onAddConditionalRule}
|
|
476
|
-
onRemoveConditionalRule={onRemoveConditionalRule}
|
|
477
|
-
onUpdateConditionalRule={onUpdateConditionalRule}
|
|
478
|
-
filterFields={filterFields}
|
|
479
|
-
currentView={view}
|
|
480
|
-
onViewChange={onViewChange}
|
|
481
|
-
lifecycleTabLabel={lifecycleDrawerLabel}
|
|
482
|
-
fieldDefinitions={fieldDefinitionsForDrawer}
|
|
483
|
-
resolveColumnLabel={resolveColumnLabel}
|
|
484
|
-
displayOptions={displayOptions}
|
|
485
|
-
onDisplayOptionsChange={onDisplayOptionsChange}
|
|
486
|
-
renderFilterOptionValue={renderFilterOptionValue}
|
|
487
|
-
extraActions={
|
|
488
|
-
<Tip side="bottom" label="Edit dashboard layout on canvas">
|
|
489
|
-
<Button
|
|
490
|
-
type="button"
|
|
491
|
-
variant="ghost"
|
|
492
|
-
size="icon-sm"
|
|
493
|
-
aria-label="Edit dashboard layout"
|
|
494
|
-
onClick={handleDashboardLayoutEditStart}
|
|
495
|
-
className="text-muted-foreground hover:text-interactive-hover-foreground hover:bg-interactive-hover"
|
|
496
|
-
>
|
|
497
|
-
<i className="fa-light fa-pen-ruler text-[13px]" aria-hidden="true" />
|
|
498
|
-
</Button>
|
|
499
|
-
</Tip>
|
|
500
|
-
}
|
|
501
|
-
/>
|
|
502
|
-
)}
|
|
503
|
-
/>
|
|
504
|
-
) : null}
|
|
505
|
-
|
|
506
|
-
{/* Contextual placement charts + KPI card (customise on canvas) */}
|
|
507
|
-
<PlacementsDashboardChartsSection
|
|
508
|
-
placements={state.rows as Placement[]}
|
|
509
|
-
keyMetrics={dashboardKpi}
|
|
510
|
-
visibleCards={visibleCards}
|
|
511
|
-
cardOrder={cardOrder}
|
|
512
|
-
cardSpans={cardSpans}
|
|
513
|
-
cardChartTypes={cardChartTypes}
|
|
514
|
-
keyMetricsKpiCount={keyMetricsKpiCount}
|
|
515
|
-
layoutEditMode={dashboardLayoutEdit}
|
|
516
|
-
onVisibleChange={handleVisibleChange}
|
|
517
|
-
onOrderChange={handleOrderChange}
|
|
518
|
-
onSpanChange={handleSpanChange}
|
|
519
|
-
onChartTypeChange={handleChartTypeChange}
|
|
520
|
-
onKeyMetricsKpiCountChange={setKeyMetricsKpiCount}
|
|
521
|
-
onResetLayout={handleResetDashboardLayout}
|
|
522
|
-
onLayoutEditDone={handleDashboardLayoutEditDone}
|
|
523
|
-
onLayoutEditCancel={handleDashboardLayoutEditCancel}
|
|
524
|
-
/>
|
|
525
|
-
</>
|
|
526
|
-
)
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// ─── Placement-specific tile for FolderGridView ──────────────────────────────
|
|
530
|
-
|
|
531
|
-
function PlacementFolderTile({
|
|
532
|
-
row,
|
|
533
|
-
tab,
|
|
534
|
-
hiddenColKeys,
|
|
535
|
-
boardColumns,
|
|
536
|
-
conditionalRules,
|
|
537
|
-
onClick,
|
|
538
|
-
}: {
|
|
539
|
-
row: Placement
|
|
540
|
-
tab: BoardCardLifecycleTabId
|
|
541
|
-
hiddenColKeys: Set<string>
|
|
542
|
-
boardColumns: ColumnDef<Placement>[]
|
|
543
|
-
conditionalRules?: ConditionalRule[]
|
|
544
|
-
onClick: () => void
|
|
545
|
-
}) {
|
|
546
|
-
const ruleBg = getConditionalRowBackground(row, conditionalRules)
|
|
547
|
-
const showStudent = isBoardFieldActive("student", tab, hiddenColKeys, boardColumns)
|
|
548
|
-
const showStatus = isBoardFieldActive("status", tab, hiddenColKeys, boardColumns)
|
|
549
|
-
const showSite = isBoardFieldActive("site", tab, hiddenColKeys, boardColumns)
|
|
550
|
-
const showSpec = isBoardFieldActive("specialization", tab, hiddenColKeys, boardColumns)
|
|
551
|
-
const showProgram = isBoardFieldActive("program", tab, hiddenColKeys, boardColumns)
|
|
552
|
-
const name = showStudent ? row.student : `Placement ${row.id}`
|
|
553
|
-
|
|
554
|
-
const statusDotClass: Record<Status, string> = {
|
|
555
|
-
confirmed: "bg-success",
|
|
556
|
-
pending: "bg-warning",
|
|
557
|
-
"under-review": "bg-brand",
|
|
558
|
-
completed: "bg-muted-foreground",
|
|
559
|
-
rejected: "bg-destructive",
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
return (
|
|
563
|
-
<button
|
|
564
|
-
type="button"
|
|
565
|
-
onClick={onClick}
|
|
566
|
-
className={`group relative flex flex-col items-center gap-2 rounded-xl border border-border bg-card p-4 text-left hover:border-interactive-hover hover:bg-interactive-hover/30 hover:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring transition-all duration-100 cursor-pointer select-none w-full ${ruleBg}`}
|
|
567
|
-
aria-label={`Open ${name}`}
|
|
568
|
-
>
|
|
569
|
-
<div className="relative">
|
|
570
|
-
<AvatarInitials initials={row.initials} className="size-14 rounded-full text-lg font-semibold" />
|
|
571
|
-
{showStatus && (
|
|
572
|
-
<span className="absolute -bottom-0.5 -right-1 flex size-4 items-center justify-center rounded-full bg-card ring-2 ring-card" aria-hidden="true">
|
|
573
|
-
<span className={`size-2.5 rounded-full ${statusDotClass[row.status]}`} />
|
|
574
|
-
</span>
|
|
575
|
-
)}
|
|
576
|
-
</div>
|
|
577
|
-
<p className="w-full text-center text-[13px] font-medium text-foreground leading-tight line-clamp-2">{name}</p>
|
|
578
|
-
{showStatus && <StatusBadge status={row.status} />}
|
|
579
|
-
{(showSite || showSpec || showProgram) && (
|
|
580
|
-
<div className="flex w-full flex-col gap-0.5">
|
|
581
|
-
{showSite && (
|
|
582
|
-
<p className="truncate text-center text-[11px] text-muted-foreground leading-tight">
|
|
583
|
-
<i className="fa-light fa-building mr-1" aria-hidden="true" />{row.site}
|
|
584
|
-
</p>
|
|
585
|
-
)}
|
|
586
|
-
{showSpec && <p className="truncate text-center text-[11px] text-muted-foreground leading-tight">{row.specialization}</p>}
|
|
587
|
-
{showProgram && <p className="truncate text-center text-[11px] text-muted-foreground leading-tight">{row.program}</p>}
|
|
588
|
-
</div>
|
|
589
|
-
)}
|
|
590
|
-
</button>
|
|
591
|
-
)
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
// ─── Folder view shell ────────────────────────────────────────────────────────
|
|
596
|
-
|
|
597
|
-
/** Folder / icon-grid view shell */
|
|
598
|
-
function DataListFolderShell({
|
|
599
|
-
state,
|
|
600
|
-
openDrawerRef,
|
|
601
|
-
tableData,
|
|
602
|
-
columns,
|
|
603
|
-
lifecycleTabId,
|
|
604
|
-
view,
|
|
605
|
-
onViewChange,
|
|
606
|
-
pagination,
|
|
607
|
-
onPaginationChange,
|
|
608
|
-
conditionalRules,
|
|
609
|
-
onAddConditionalRule,
|
|
610
|
-
onRemoveConditionalRule,
|
|
611
|
-
onUpdateConditionalRule,
|
|
612
|
-
filterFields,
|
|
613
|
-
lifecycleDrawerLabel,
|
|
614
|
-
fieldDefinitionsForDrawer,
|
|
615
|
-
resolveColumnLabel,
|
|
616
|
-
renderFilterOptionValue,
|
|
617
|
-
displayOptions,
|
|
618
|
-
onDisplayOptionsChange,
|
|
619
|
-
listRows,
|
|
620
|
-
emptyTableCopy,
|
|
621
|
-
}: {
|
|
622
|
-
state: ReturnType<typeof useTableState<Placement>>
|
|
623
|
-
openDrawerRef: React.MutableRefObject<() => void>
|
|
624
|
-
tableData: Placement[]
|
|
625
|
-
columns: ColumnDef<Placement>[]
|
|
626
|
-
lifecycleTabId: PlacementLifecycleTabId
|
|
627
|
-
view: DataListViewType
|
|
628
|
-
onViewChange?: (view: DataListViewType) => void
|
|
629
|
-
pagination: boolean
|
|
630
|
-
onPaginationChange: (v: boolean) => void
|
|
631
|
-
conditionalRules: ConditionalRule[]
|
|
632
|
-
onAddConditionalRule: (rule: Omit<ConditionalRule, "id">) => void
|
|
633
|
-
onRemoveConditionalRule: (id: string) => void
|
|
634
|
-
onUpdateConditionalRule: (id: string, patch: Partial<ConditionalRule>) => void
|
|
635
|
-
filterFields: FilterFieldDef[]
|
|
636
|
-
lifecycleDrawerLabel: string
|
|
637
|
-
fieldDefinitionsForDrawer: { key: string; label: string; sortable?: boolean }[]
|
|
638
|
-
resolveColumnLabel: (key: string) => string
|
|
639
|
-
renderFilterOptionValue: (fieldKey: string, value: string) => React.ReactNode
|
|
640
|
-
displayOptions: DataListDisplayOptions
|
|
641
|
-
onDisplayOptionsChange: (patch: Partial<DataListDisplayOptions>) => void
|
|
642
|
-
listRows: Placement[]
|
|
643
|
-
emptyTableCopy: string
|
|
644
|
-
}) {
|
|
645
|
-
const router = useRouter()
|
|
646
|
-
|
|
647
|
-
// Stable "open properties drawer" callback ref — see top of this file.
|
|
648
|
-
React.useEffect(() => {
|
|
649
|
-
openDrawerRef.current = () => state.setSheetOpen(true)
|
|
650
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
651
|
-
}, [openDrawerRef, state.setSheetOpen])
|
|
652
|
-
|
|
653
|
-
const boardColumns = state.displayCols.filter(c => c.key !== "select" && c.key !== "actions")
|
|
654
|
-
|
|
655
|
-
return (
|
|
656
|
-
<>
|
|
657
|
-
<DataTableToolbar
|
|
658
|
-
state={state}
|
|
659
|
-
columns={columns}
|
|
660
|
-
searchable
|
|
661
|
-
renderFilterOptionValue={renderFilterOptionValue}
|
|
662
|
-
searchAriaLabel="Search rows"
|
|
663
|
-
toolbarSlot={s => (
|
|
664
|
-
<TablePropertiesDrawerButton
|
|
665
|
-
state={s}
|
|
666
|
-
totalRows={tableData.length}
|
|
667
|
-
pagination={pagination}
|
|
668
|
-
onPaginationChange={onPaginationChange}
|
|
669
|
-
conditionalRules={conditionalRules}
|
|
670
|
-
onAddConditionalRule={onAddConditionalRule}
|
|
671
|
-
onRemoveConditionalRule={onRemoveConditionalRule}
|
|
672
|
-
onUpdateConditionalRule={onUpdateConditionalRule}
|
|
673
|
-
filterFields={filterFields}
|
|
674
|
-
currentView={view}
|
|
675
|
-
onViewChange={onViewChange}
|
|
676
|
-
lifecycleTabLabel={lifecycleDrawerLabel}
|
|
677
|
-
fieldDefinitions={fieldDefinitionsForDrawer}
|
|
678
|
-
resolveColumnLabel={resolveColumnLabel}
|
|
679
|
-
displayOptions={displayOptions}
|
|
680
|
-
onDisplayOptionsChange={onDisplayOptionsChange}
|
|
681
|
-
renderFilterOptionValue={renderFilterOptionValue}
|
|
682
|
-
/>
|
|
683
|
-
)}
|
|
684
|
-
/>
|
|
685
|
-
<FolderGridView<Placement>
|
|
686
|
-
rows={listRows}
|
|
687
|
-
getRowId={r => r.id}
|
|
688
|
-
ariaLabel="Demo folder view"
|
|
689
|
-
emptyContent={<p>{emptyTableCopy}</p>}
|
|
690
|
-
renderTile={row => (
|
|
691
|
-
<PlacementFolderTile
|
|
692
|
-
row={row}
|
|
693
|
-
tab={lifecycleTabId as BoardCardLifecycleTabId}
|
|
694
|
-
hiddenColKeys={state.hiddenCols}
|
|
695
|
-
boardColumns={boardColumns}
|
|
696
|
-
conditionalRules={conditionalRules}
|
|
697
|
-
onClick={() => router.push(`/data-list/${row.id}`)}
|
|
698
|
-
/>
|
|
699
|
-
)}
|
|
700
|
-
/>
|
|
701
|
-
</>
|
|
702
|
-
)
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
// ─── Tree / outline + details shell ───────────────────────────────────────────
|
|
706
|
-
|
|
707
|
-
function DataListTreeShell({
|
|
708
|
-
state,
|
|
709
|
-
openDrawerRef,
|
|
710
|
-
tableData,
|
|
711
|
-
columns,
|
|
712
|
-
lifecycleTabId,
|
|
713
|
-
view,
|
|
714
|
-
onViewChange,
|
|
715
|
-
pagination,
|
|
716
|
-
onPaginationChange,
|
|
717
|
-
conditionalRules,
|
|
718
|
-
onAddConditionalRule,
|
|
719
|
-
onRemoveConditionalRule,
|
|
720
|
-
onUpdateConditionalRule,
|
|
721
|
-
filterFields,
|
|
722
|
-
lifecycleDrawerLabel,
|
|
723
|
-
fieldDefinitionsForDrawer,
|
|
724
|
-
resolveColumnLabel,
|
|
725
|
-
renderFilterOptionValue,
|
|
726
|
-
displayOptions,
|
|
727
|
-
onDisplayOptionsChange,
|
|
728
|
-
listRows,
|
|
729
|
-
emptyTableCopy,
|
|
730
|
-
}: {
|
|
731
|
-
state: ReturnType<typeof useTableState<Placement>>
|
|
732
|
-
openDrawerRef: React.MutableRefObject<() => void>
|
|
733
|
-
tableData: Placement[]
|
|
734
|
-
columns: ColumnDef<Placement>[]
|
|
735
|
-
lifecycleTabId: PlacementLifecycleTabId
|
|
736
|
-
view: DataListViewType
|
|
737
|
-
onViewChange?: (view: DataListViewType) => void
|
|
738
|
-
pagination: boolean
|
|
739
|
-
onPaginationChange: (v: boolean) => void
|
|
740
|
-
conditionalRules: ConditionalRule[]
|
|
741
|
-
onAddConditionalRule: (rule: Omit<ConditionalRule, "id">) => void
|
|
742
|
-
onRemoveConditionalRule: (id: string) => void
|
|
743
|
-
onUpdateConditionalRule: (id: string, patch: Partial<ConditionalRule>) => void
|
|
744
|
-
filterFields: FilterFieldDef[]
|
|
745
|
-
lifecycleDrawerLabel: string
|
|
746
|
-
fieldDefinitionsForDrawer: { key: string; label: string; sortable?: boolean }[]
|
|
747
|
-
resolveColumnLabel: (key: string) => string
|
|
748
|
-
renderFilterOptionValue: (fieldKey: string, value: string) => React.ReactNode
|
|
749
|
-
displayOptions: DataListDisplayOptions
|
|
750
|
-
onDisplayOptionsChange: (patch: Partial<DataListDisplayOptions>) => void
|
|
751
|
-
listRows: Placement[]
|
|
752
|
-
emptyTableCopy: string
|
|
753
|
-
}) {
|
|
754
|
-
const boardColumns = state.displayCols.filter(c => c.key !== "select" && c.key !== "actions")
|
|
755
|
-
const [selectedId, setSelectedId] = React.useState<number | null>(() => listRows[0]?.id ?? null)
|
|
756
|
-
|
|
757
|
-
// Stable "open properties drawer" callback ref — see top of this file.
|
|
758
|
-
React.useEffect(() => {
|
|
759
|
-
openDrawerRef.current = () => state.setSheetOpen(true)
|
|
760
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
761
|
-
}, [openDrawerRef, state.setSheetOpen])
|
|
762
|
-
|
|
763
|
-
React.useEffect(() => {
|
|
764
|
-
if (selectedId == null) {
|
|
765
|
-
setSelectedId(listRows[0]?.id ?? null)
|
|
766
|
-
return
|
|
767
|
-
}
|
|
768
|
-
if (!listRows.some(r => r.id === selectedId)) {
|
|
769
|
-
setSelectedId(listRows[0]?.id ?? null)
|
|
770
|
-
}
|
|
771
|
-
}, [listRows, selectedId])
|
|
772
|
-
|
|
773
|
-
const selected = listRows.find(r => r.id === selectedId) ?? null
|
|
774
|
-
|
|
775
|
-
return (
|
|
776
|
-
<>
|
|
777
|
-
<DataTableToolbar
|
|
778
|
-
state={state}
|
|
779
|
-
columns={columns}
|
|
780
|
-
searchable
|
|
781
|
-
renderFilterOptionValue={renderFilterOptionValue}
|
|
782
|
-
searchAriaLabel="Search rows"
|
|
783
|
-
toolbarSlot={s => (
|
|
784
|
-
<TablePropertiesDrawerButton
|
|
785
|
-
state={s}
|
|
786
|
-
totalRows={tableData.length}
|
|
787
|
-
pagination={pagination}
|
|
788
|
-
onPaginationChange={onPaginationChange}
|
|
789
|
-
conditionalRules={conditionalRules}
|
|
790
|
-
onAddConditionalRule={onAddConditionalRule}
|
|
791
|
-
onRemoveConditionalRule={onRemoveConditionalRule}
|
|
792
|
-
onUpdateConditionalRule={onUpdateConditionalRule}
|
|
793
|
-
filterFields={filterFields}
|
|
794
|
-
currentView={view}
|
|
795
|
-
onViewChange={onViewChange}
|
|
796
|
-
lifecycleTabLabel={lifecycleDrawerLabel}
|
|
797
|
-
fieldDefinitions={fieldDefinitionsForDrawer}
|
|
798
|
-
resolveColumnLabel={resolveColumnLabel}
|
|
799
|
-
displayOptions={displayOptions}
|
|
800
|
-
onDisplayOptionsChange={onDisplayOptionsChange}
|
|
801
|
-
renderFilterOptionValue={renderFilterOptionValue}
|
|
802
|
-
/>
|
|
803
|
-
)}
|
|
804
|
-
/>
|
|
805
|
-
<ListPageTreePanelShell
|
|
806
|
-
resizableGroupId={`data-list-tree-${lifecycleTabId}`}
|
|
807
|
-
ariaLabel="Record outline and details"
|
|
808
|
-
tree={
|
|
809
|
-
<div className="flex min-h-0 flex-1 flex-col">
|
|
810
|
-
<ListPageTreeColumnHeader title="Records" />
|
|
811
|
-
{listRows.length === 0 ? (
|
|
812
|
-
<p className="p-3 text-sm text-muted-foreground">{emptyTableCopy}</p>
|
|
813
|
-
) : (
|
|
814
|
-
<ul
|
|
815
|
-
role="tree"
|
|
816
|
-
aria-label="Demo records"
|
|
817
|
-
className="min-h-0 flex-1 list-none space-y-0.5 overflow-y-auto py-1"
|
|
818
|
-
>
|
|
819
|
-
{listRows.map(row => {
|
|
820
|
-
const isSel = selectedId === row.id
|
|
821
|
-
return (
|
|
822
|
-
<li key={row.id} role="none" className="py-0.5">
|
|
823
|
-
<button
|
|
824
|
-
type="button"
|
|
825
|
-
role="treeitem"
|
|
826
|
-
aria-selected={isSel}
|
|
827
|
-
tabIndex={isSel ? 0 : -1}
|
|
828
|
-
onClick={() => setSelectedId(row.id)}
|
|
829
|
-
className={cn(
|
|
830
|
-
"flex w-full min-h-8 items-center rounded-md px-3 py-2 text-left text-sm transition-colors duration-75",
|
|
831
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
|
|
832
|
-
isSel
|
|
833
|
-
? "bg-accent font-medium text-accent-foreground"
|
|
834
|
-
: "text-foreground hover:bg-muted/50",
|
|
835
|
-
)}
|
|
836
|
-
>
|
|
837
|
-
<span className="min-w-0 truncate">{row.student}</span>
|
|
838
|
-
</button>
|
|
839
|
-
</li>
|
|
840
|
-
)
|
|
841
|
-
})}
|
|
842
|
-
</ul>
|
|
843
|
-
)}
|
|
844
|
-
</div>
|
|
845
|
-
}
|
|
846
|
-
details={
|
|
847
|
-
selected ? (
|
|
848
|
-
<div className="flex min-h-0 flex-1 flex-col overflow-hidden bg-card">
|
|
849
|
-
<ListPageTreeColumnHeader title="Details" />
|
|
850
|
-
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
851
|
-
<PlacementFinderDetail
|
|
852
|
-
row={selected}
|
|
853
|
-
tab={lifecycleTabId as BoardCardLifecycleTabId}
|
|
854
|
-
hiddenColKeys={state.hiddenCols}
|
|
855
|
-
boardColumns={boardColumns}
|
|
856
|
-
/>
|
|
857
|
-
</div>
|
|
858
|
-
</div>
|
|
859
|
-
) : (
|
|
860
|
-
<ListPageSplitDetailsPlaceholder title="Nothing selected" />
|
|
861
|
-
)
|
|
862
|
-
}
|
|
863
|
-
/>
|
|
864
|
-
</>
|
|
865
|
-
)
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
// ─── Placement-specific list row for FinderPanelView ─────────────────────────
|
|
869
|
-
|
|
870
|
-
function PlacementFinderListRow({
|
|
871
|
-
row,
|
|
872
|
-
isSelected,
|
|
873
|
-
tab,
|
|
874
|
-
hiddenColKeys,
|
|
875
|
-
boardColumns,
|
|
876
|
-
conditionalRules,
|
|
877
|
-
}: {
|
|
878
|
-
row: Placement
|
|
879
|
-
isSelected: boolean
|
|
880
|
-
tab: BoardCardLifecycleTabId
|
|
881
|
-
hiddenColKeys: Set<string>
|
|
882
|
-
boardColumns: ColumnDef<Placement>[]
|
|
883
|
-
conditionalRules?: ConditionalRule[]
|
|
884
|
-
}) {
|
|
885
|
-
const ruleBg = getConditionalRowBackground(row, conditionalRules)
|
|
886
|
-
const showStudent = isBoardFieldActive("student", tab, hiddenColKeys, boardColumns)
|
|
887
|
-
const showSite = isBoardFieldActive("site", tab, hiddenColKeys, boardColumns)
|
|
888
|
-
const name = showStudent ? row.student : `Placement ${row.id}`
|
|
889
|
-
|
|
890
|
-
return (
|
|
891
|
-
<div
|
|
892
|
-
className={`flex w-full min-w-0 items-center gap-3 transition-colors duration-75 ${
|
|
893
|
-
isSelected
|
|
894
|
-
? "bg-transparent text-accent-foreground"
|
|
895
|
-
: cn("text-foreground", ruleBg)
|
|
896
|
-
}`}
|
|
897
|
-
>
|
|
898
|
-
<AvatarInitials
|
|
899
|
-
initials={row.initials}
|
|
900
|
-
className={cn(
|
|
901
|
-
"size-8 shrink-0 rounded-full text-[11px] font-semibold",
|
|
902
|
-
isSelected ? "ring-2 ring-accent-foreground/35" : "",
|
|
903
|
-
)}
|
|
904
|
-
/>
|
|
905
|
-
<div className="min-w-0 flex-1">
|
|
906
|
-
<p className={cn("truncate text-[13px] font-medium leading-tight", isSelected ? "text-accent-foreground" : "text-foreground")}>
|
|
907
|
-
{name}
|
|
908
|
-
</p>
|
|
909
|
-
{showSite && (
|
|
910
|
-
<p className={cn("mt-0.5 truncate text-[11px] leading-tight", isSelected ? "text-accent-foreground/80" : "text-muted-foreground")}>
|
|
911
|
-
{row.site}
|
|
912
|
-
</p>
|
|
913
|
-
)}
|
|
914
|
-
</div>
|
|
915
|
-
{!isSelected && <StatusBadge status={row.status} />}
|
|
916
|
-
</div>
|
|
917
|
-
)
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
// ─── Placement-specific detail pane for FinderPanelView ──────────────────────
|
|
921
|
-
|
|
922
|
-
function PlacementFinderDetail({
|
|
923
|
-
row,
|
|
924
|
-
tab,
|
|
925
|
-
hiddenColKeys,
|
|
926
|
-
boardColumns,
|
|
927
|
-
}: {
|
|
928
|
-
row: Placement
|
|
929
|
-
tab: BoardCardLifecycleTabId
|
|
930
|
-
hiddenColKeys: Set<string>
|
|
931
|
-
boardColumns: ColumnDef<Placement>[]
|
|
932
|
-
}) {
|
|
933
|
-
const router = useRouter()
|
|
934
|
-
const show = (key: string) => isBoardFieldActive(key, tab, hiddenColKeys, boardColumns)
|
|
935
|
-
|
|
936
|
-
return (
|
|
937
|
-
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
938
|
-
{/* Header */}
|
|
939
|
-
<div className="flex shrink-0 items-start gap-4 border-b border-border px-5 py-4">
|
|
940
|
-
<AvatarInitials initials={row.initials} className="size-14 shrink-0 rounded-full text-lg font-semibold" />
|
|
941
|
-
<div className="min-w-0 flex-1">
|
|
942
|
-
<h2 className="text-base font-semibold text-foreground leading-tight">{row.student}</h2>
|
|
943
|
-
{show("program") && <p className="mt-0.5 text-[13px] text-muted-foreground">{row.program}</p>}
|
|
944
|
-
{show("status") && <div className="mt-2"><StatusBadge status={row.status} /></div>}
|
|
945
|
-
</div>
|
|
946
|
-
<Tip side="bottom" label="Open full detail page">
|
|
947
|
-
<Button type="button" variant="outline" size="sm" className="shrink-0"
|
|
948
|
-
onClick={() => router.push(`/data-list/${row.id}`)}
|
|
949
|
-
aria-label={`Open full detail for ${row.student}`}>
|
|
950
|
-
<i className="fa-light fa-arrow-up-right-from-square text-[12px]" aria-hidden="true" />
|
|
951
|
-
Open
|
|
952
|
-
</Button>
|
|
953
|
-
</Tip>
|
|
954
|
-
</div>
|
|
955
|
-
|
|
956
|
-
{/* Fields */}
|
|
957
|
-
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
|
|
958
|
-
<dl className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
959
|
-
{show("email") && (
|
|
960
|
-
<div className="flex flex-col gap-0.5">
|
|
961
|
-
<dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
|
|
962
|
-
<i className="fa-light fa-envelope text-[10px]" aria-hidden="true" /> Email
|
|
963
|
-
</dt>
|
|
964
|
-
<dd className="text-[13px]">
|
|
965
|
-
<a href={mailtoHref(row.email)} className="text-interactive-foreground hover:underline">{row.email}</a>
|
|
966
|
-
</dd>
|
|
967
|
-
</div>
|
|
968
|
-
)}
|
|
969
|
-
{show("site") && (
|
|
970
|
-
<div className="flex flex-col gap-0.5">
|
|
971
|
-
<dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
|
|
972
|
-
<i className="fa-light fa-building text-[10px]" aria-hidden="true" /> Site
|
|
973
|
-
</dt>
|
|
974
|
-
<dd className="text-[13px] text-foreground">{row.site}</dd>
|
|
975
|
-
</div>
|
|
976
|
-
)}
|
|
977
|
-
{show("internship") && (
|
|
978
|
-
<div className="flex flex-col gap-0.5">
|
|
979
|
-
<dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
|
|
980
|
-
<i className="fa-light fa-briefcase text-[10px]" aria-hidden="true" /> Internship
|
|
981
|
-
</dt>
|
|
982
|
-
<dd className="text-[13px] text-foreground">{row.internship}</dd>
|
|
983
|
-
</div>
|
|
984
|
-
)}
|
|
985
|
-
{show("specialization") && (
|
|
986
|
-
<div className="flex flex-col gap-0.5">
|
|
987
|
-
<dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
|
|
988
|
-
<i className="fa-light fa-stethoscope text-[10px]" aria-hidden="true" /> Specialization
|
|
989
|
-
</dt>
|
|
990
|
-
<dd className="text-[13px] text-foreground">{row.specialization}</dd>
|
|
991
|
-
</div>
|
|
992
|
-
)}
|
|
993
|
-
{show("supervisor") && (
|
|
994
|
-
<div className="flex flex-col gap-0.5">
|
|
995
|
-
<dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
|
|
996
|
-
<i className="fa-light fa-user-tie text-[10px]" aria-hidden="true" /> Supervisor
|
|
997
|
-
</dt>
|
|
998
|
-
<dd className="text-[13px] text-foreground">{row.supervisor}</dd>
|
|
999
|
-
</div>
|
|
1000
|
-
)}
|
|
1001
|
-
{show("start") && (
|
|
1002
|
-
<div className="flex flex-col gap-0.5">
|
|
1003
|
-
<dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
|
|
1004
|
-
<i className="fa-light fa-calendar text-[10px]" aria-hidden="true" /> Start Date
|
|
1005
|
-
</dt>
|
|
1006
|
-
<dd className="text-[13px] text-foreground">{row.start}</dd>
|
|
1007
|
-
</div>
|
|
1008
|
-
)}
|
|
1009
|
-
{show("duration") && (
|
|
1010
|
-
<div className="flex flex-col gap-0.5">
|
|
1011
|
-
<dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
|
|
1012
|
-
<i className="fa-light fa-clock text-[10px]" aria-hidden="true" /> Duration
|
|
1013
|
-
</dt>
|
|
1014
|
-
<dd className="text-[13px] text-foreground">{row.duration}</dd>
|
|
1015
|
-
</div>
|
|
1016
|
-
)}
|
|
1017
|
-
{tab === "ongoing" && (
|
|
1018
|
-
<div className="flex flex-col gap-0.5 sm:col-span-2">
|
|
1019
|
-
<dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
|
|
1020
|
-
<i className="fa-light fa-chart-line text-[10px]" aria-hidden="true" /> Progress
|
|
1021
|
-
</dt>
|
|
1022
|
-
<dd className="text-[13px] text-foreground flex flex-col gap-1.5">
|
|
1023
|
-
<span>{row.progressWeeksDone} / {row.progressWeeksTotal} weeks</span>
|
|
1024
|
-
<div role="progressbar" aria-valuenow={row.progressWeeksDone} aria-valuemin={0} aria-valuemax={row.progressWeeksTotal}
|
|
1025
|
-
aria-label={`${row.progressWeeksDone} of ${row.progressWeeksTotal} weeks completed`}
|
|
1026
|
-
className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
|
|
1027
|
-
<div className="h-full rounded-full bg-primary transition-all"
|
|
1028
|
-
style={{ width: `${Math.round((row.progressWeeksDone / Math.max(1, row.progressWeeksTotal)) * 100)}%` }} />
|
|
1029
|
-
</div>
|
|
1030
|
-
</dd>
|
|
1031
|
-
</div>
|
|
1032
|
-
)}
|
|
1033
|
-
{row.siteAddress && (
|
|
1034
|
-
<div className="flex flex-col gap-0.5 sm:col-span-2">
|
|
1035
|
-
<dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
|
|
1036
|
-
<i className="fa-light fa-location-dot text-[10px]" aria-hidden="true" /> Site Address
|
|
1037
|
-
</dt>
|
|
1038
|
-
<dd className="text-[13px] text-foreground">{row.siteAddress}</dd>
|
|
1039
|
-
</div>
|
|
1040
|
-
)}
|
|
1041
|
-
</dl>
|
|
1042
|
-
</div>
|
|
1043
|
-
</div>
|
|
1044
|
-
)
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
// ─── Status groups for FinderPanelView ───────────────────────────────────────
|
|
1048
|
-
|
|
1049
|
-
const STATUS_GROUPS: Array<{ id: Status | "all"; label: string; accent: string }> = [
|
|
1050
|
-
{ id: "all", label: "All", accent: "bg-muted-foreground" },
|
|
1051
|
-
{ id: "confirmed", label: "Confirmed", accent: "bg-success" },
|
|
1052
|
-
{ id: "pending", label: "Pending", accent: "bg-warning" },
|
|
1053
|
-
{ id: "under-review", label: "Under Review", accent: "bg-brand" },
|
|
1054
|
-
{ id: "rejected", label: "Rejected", accent: "bg-destructive" },
|
|
1055
|
-
{ id: "completed", label: "Completed", accent: "bg-muted-foreground/50" },
|
|
1056
|
-
]
|
|
1057
|
-
|
|
1058
|
-
function buildStatusGroups(rows: Placement[]): FinderGroup[] {
|
|
1059
|
-
return STATUS_GROUPS.map(sg => ({
|
|
1060
|
-
id: sg.id,
|
|
1061
|
-
label: sg.label,
|
|
1062
|
-
accent: sg.accent,
|
|
1063
|
-
count: sg.id === "all" ? rows.length : rows.filter(r => r.status === sg.id).length,
|
|
1064
|
-
}))
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
// ─── Panel view shell ────────────────────────────────────────────────────────
|
|
1068
|
-
|
|
1069
|
-
/** Finder-style panel view shell with groups, list, and detail pane */
|
|
1070
|
-
function DataListPanelShell({
|
|
1071
|
-
state,
|
|
1072
|
-
openDrawerRef,
|
|
1073
|
-
tableData,
|
|
1074
|
-
columns,
|
|
1075
|
-
lifecycleTabId,
|
|
1076
|
-
view,
|
|
1077
|
-
onViewChange,
|
|
1078
|
-
pagination,
|
|
1079
|
-
onPaginationChange,
|
|
1080
|
-
conditionalRules,
|
|
1081
|
-
onAddConditionalRule,
|
|
1082
|
-
onRemoveConditionalRule,
|
|
1083
|
-
onUpdateConditionalRule,
|
|
1084
|
-
filterFields,
|
|
1085
|
-
lifecycleDrawerLabel,
|
|
1086
|
-
fieldDefinitionsForDrawer,
|
|
1087
|
-
resolveColumnLabel,
|
|
1088
|
-
renderFilterOptionValue,
|
|
1089
|
-
displayOptions,
|
|
1090
|
-
onDisplayOptionsChange,
|
|
1091
|
-
listRows,
|
|
1092
|
-
emptyTableCopy,
|
|
1093
|
-
panelGroupsBuilder,
|
|
1094
|
-
panelRenderListRow,
|
|
1095
|
-
panelRenderDetail,
|
|
1096
|
-
}: {
|
|
1097
|
-
state: ReturnType<typeof useTableState<Placement>>
|
|
1098
|
-
openDrawerRef: React.MutableRefObject<() => void>
|
|
1099
|
-
tableData: Placement[]
|
|
1100
|
-
columns: ColumnDef<Placement>[]
|
|
1101
|
-
lifecycleTabId: PlacementLifecycleTabId
|
|
1102
|
-
view: DataListViewType
|
|
1103
|
-
onViewChange?: (view: DataListViewType) => void
|
|
1104
|
-
pagination: boolean
|
|
1105
|
-
onPaginationChange: (v: boolean) => void
|
|
1106
|
-
conditionalRules: ConditionalRule[]
|
|
1107
|
-
onAddConditionalRule: (rule: Omit<ConditionalRule, "id">) => void
|
|
1108
|
-
onRemoveConditionalRule: (id: string) => void
|
|
1109
|
-
onUpdateConditionalRule: (id: string, patch: Partial<ConditionalRule>) => void
|
|
1110
|
-
filterFields: FilterFieldDef[]
|
|
1111
|
-
lifecycleDrawerLabel: string
|
|
1112
|
-
fieldDefinitionsForDrawer: { key: string; label: string; sortable?: boolean }[]
|
|
1113
|
-
resolveColumnLabel: (key: string) => string
|
|
1114
|
-
renderFilterOptionValue: (fieldKey: string, value: string) => React.ReactNode
|
|
1115
|
-
displayOptions: DataListDisplayOptions
|
|
1116
|
-
onDisplayOptionsChange: (patch: Partial<DataListDisplayOptions>) => void
|
|
1117
|
-
listRows: Placement[]
|
|
1118
|
-
emptyTableCopy: string
|
|
1119
|
-
panelGroupsBuilder?: (rows: Placement[]) => FinderGroup[]
|
|
1120
|
-
panelRenderListRow?: (row: Placement, isSelected: boolean) => React.ReactNode
|
|
1121
|
-
panelRenderDetail?: (row: Placement) => React.ReactNode
|
|
1122
|
-
}) {
|
|
1123
|
-
// Stable "open properties drawer" callback ref — see top of this file.
|
|
1124
|
-
React.useEffect(() => {
|
|
1125
|
-
openDrawerRef.current = () => state.setSheetOpen(true)
|
|
1126
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1127
|
-
}, [openDrawerRef, state.setSheetOpen])
|
|
1128
|
-
|
|
1129
|
-
const boardColumns = state.displayCols.filter(c => c.key !== "select" && c.key !== "actions")
|
|
1130
|
-
const groups = React.useMemo(
|
|
1131
|
-
() => panelGroupsBuilder ? panelGroupsBuilder(listRows) : buildStatusGroups(listRows),
|
|
1132
|
-
[listRows, panelGroupsBuilder],
|
|
1133
|
-
)
|
|
1134
|
-
|
|
1135
|
-
return (
|
|
1136
|
-
<div className="flex min-h-0 flex-1 flex-col">
|
|
1137
|
-
<DataTableToolbar
|
|
1138
|
-
state={state}
|
|
1139
|
-
columns={columns}
|
|
1140
|
-
searchable
|
|
1141
|
-
renderFilterOptionValue={renderFilterOptionValue}
|
|
1142
|
-
searchAriaLabel="Search rows"
|
|
1143
|
-
toolbarSlot={s => (
|
|
1144
|
-
<TablePropertiesDrawerButton
|
|
1145
|
-
state={s}
|
|
1146
|
-
totalRows={tableData.length}
|
|
1147
|
-
pagination={pagination}
|
|
1148
|
-
onPaginationChange={onPaginationChange}
|
|
1149
|
-
conditionalRules={conditionalRules}
|
|
1150
|
-
onAddConditionalRule={onAddConditionalRule}
|
|
1151
|
-
onRemoveConditionalRule={onRemoveConditionalRule}
|
|
1152
|
-
onUpdateConditionalRule={onUpdateConditionalRule}
|
|
1153
|
-
filterFields={filterFields}
|
|
1154
|
-
currentView={view}
|
|
1155
|
-
onViewChange={onViewChange}
|
|
1156
|
-
lifecycleTabLabel={lifecycleDrawerLabel}
|
|
1157
|
-
fieldDefinitions={fieldDefinitionsForDrawer}
|
|
1158
|
-
resolveColumnLabel={resolveColumnLabel}
|
|
1159
|
-
displayOptions={displayOptions}
|
|
1160
|
-
onDisplayOptionsChange={onDisplayOptionsChange}
|
|
1161
|
-
renderFilterOptionValue={renderFilterOptionValue}
|
|
1162
|
-
/>
|
|
1163
|
-
)}
|
|
1164
|
-
/>
|
|
1165
|
-
<ListPageSplitHubChrome aria-label={lifecycleDrawerLabel}>
|
|
1166
|
-
<FinderPanelView<Placement>
|
|
1167
|
-
embedded
|
|
1168
|
-
groupsColumnTitle="Status"
|
|
1169
|
-
groups={groups}
|
|
1170
|
-
rows={listRows}
|
|
1171
|
-
getRowId={r => r.id}
|
|
1172
|
-
getRowGroupId={r => r.status}
|
|
1173
|
-
defaultGroupId="all"
|
|
1174
|
-
autoSaveId="finder-panel-view"
|
|
1175
|
-
ariaLabel="Demo panel view"
|
|
1176
|
-
emptyList={<p>{emptyTableCopy}</p>}
|
|
1177
|
-
renderListRow={
|
|
1178
|
-
panelRenderListRow
|
|
1179
|
-
? panelRenderListRow
|
|
1180
|
-
: (row, isSelected) => (
|
|
1181
|
-
<PlacementFinderListRow
|
|
1182
|
-
row={row}
|
|
1183
|
-
isSelected={isSelected}
|
|
1184
|
-
tab={lifecycleTabId as BoardCardLifecycleTabId}
|
|
1185
|
-
hiddenColKeys={state.hiddenCols}
|
|
1186
|
-
boardColumns={boardColumns}
|
|
1187
|
-
conditionalRules={conditionalRules}
|
|
1188
|
-
/>
|
|
1189
|
-
)
|
|
1190
|
-
}
|
|
1191
|
-
renderDetail={
|
|
1192
|
-
panelRenderDetail
|
|
1193
|
-
? panelRenderDetail
|
|
1194
|
-
: row => (
|
|
1195
|
-
<PlacementFinderDetail
|
|
1196
|
-
row={row}
|
|
1197
|
-
tab={lifecycleTabId as BoardCardLifecycleTabId}
|
|
1198
|
-
hiddenColKeys={state.hiddenCols}
|
|
1199
|
-
boardColumns={boardColumns}
|
|
1200
|
-
/>
|
|
1201
|
-
)
|
|
1202
|
-
}
|
|
1203
|
-
/>
|
|
1204
|
-
</ListPageSplitHubChrome>
|
|
1205
|
-
</div>
|
|
1206
|
-
)
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1210
|
-
// Props
|
|
1211
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1212
|
-
|
|
1213
|
-
export interface PlacementsTableProps {
|
|
1214
|
-
view?: DataListViewType
|
|
1215
|
-
onViewChange?: (view: DataListViewType) => void
|
|
1216
|
-
/** Demo row segment: drives filtered rows + column set (`all` | `upcoming` | `ongoing` | `completed`). */
|
|
1217
|
-
lifecycleTabId?: PlacementLifecycleTabId
|
|
1218
|
-
/** Shared display options (persist at page level — all view types). */
|
|
1219
|
-
displayOptions?: DataListDisplayOptions
|
|
1220
|
-
onDisplayOptionsChange?: (patch: Partial<DataListDisplayOptions>) => void
|
|
1221
|
-
/** Lifecycle column set from the placements page (e.g. `getPlacementColumnsForLifecycle`). */
|
|
1222
|
-
getColumnsForLifecycle: (tab: PlacementLifecycleTabId) => ColumnDef<Placement>[]
|
|
1223
|
-
/** Empty-state copy for the active lifecycle tab — from the page. */
|
|
1224
|
-
emptyTableCopy: string
|
|
1225
|
-
/** Table Properties drawer lifecycle label — from the page. */
|
|
1226
|
-
lifecycleDrawerLabel: string
|
|
1227
|
-
/** Panel view: custom groups builder. If not provided, uses default placement status groups. */
|
|
1228
|
-
panelGroupsBuilder?: (rows: Placement[]) => FinderGroup[]
|
|
1229
|
-
/** Panel view: custom list row renderer. If not provided, uses default placement row rendering. */
|
|
1230
|
-
panelRenderListRow?: (row: Placement, isSelected: boolean) => React.ReactNode
|
|
1231
|
-
/** Panel view: custom detail pane renderer. If not provided, uses default placement detail rendering. */
|
|
1232
|
-
panelRenderDetail?: (row: Placement) => React.ReactNode
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
/** Imperative handle — open Table Properties (table view only). */
|
|
1236
|
-
export type PlacementsTableHandle = OpenTablePropertiesHandle
|
|
1237
|
-
|
|
1238
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1239
|
-
// Main component
|
|
1240
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1241
|
-
|
|
1242
|
-
export const PlacementsTable = React.forwardRef<PlacementsTableHandle, PlacementsTableProps>(function PlacementsTable({
|
|
1243
|
-
view = "table",
|
|
1244
|
-
onViewChange,
|
|
1245
|
-
lifecycleTabId = "all",
|
|
1246
|
-
displayOptions: displayOptionsProp,
|
|
1247
|
-
onDisplayOptionsChange,
|
|
1248
|
-
getColumnsForLifecycle,
|
|
1249
|
-
emptyTableCopy,
|
|
1250
|
-
lifecycleDrawerLabel,
|
|
1251
|
-
panelGroupsBuilder,
|
|
1252
|
-
panelRenderListRow,
|
|
1253
|
-
panelRenderDetail,
|
|
1254
|
-
}, ref) {
|
|
1255
|
-
const displayOptions = React.useMemo(
|
|
1256
|
-
() => ({ ...DEFAULT_DATA_LIST_DISPLAY_OPTIONS, ...displayOptionsProp }),
|
|
1257
|
-
[displayOptionsProp],
|
|
1258
|
-
)
|
|
1259
|
-
|
|
1260
|
-
const patchDisplayOptions = React.useCallback(
|
|
1261
|
-
(patch: Partial<DataListDisplayOptions>) => {
|
|
1262
|
-
onDisplayOptionsChange?.(patch)
|
|
1263
|
-
},
|
|
1264
|
-
[onDisplayOptionsChange],
|
|
1265
|
-
)
|
|
1266
|
-
const openDrawerRef = React.useRef<() => void>(() => {})
|
|
1267
|
-
|
|
1268
|
-
React.useImperativeHandle(ref, () => ({
|
|
1269
|
-
openPropertiesDrawer: () => {
|
|
1270
|
-
openDrawerRef.current()
|
|
1271
|
-
},
|
|
1272
|
-
}), [])
|
|
1273
|
-
|
|
1274
|
-
const router = useRouter()
|
|
1275
|
-
const [pagination, setPagination] = React.useState(false)
|
|
1276
|
-
|
|
1277
|
-
const columns = React.useMemo(
|
|
1278
|
-
() => getColumnsForLifecycle(lifecycleTabId),
|
|
1279
|
-
[getColumnsForLifecycle, lifecycleTabId],
|
|
1280
|
-
)
|
|
1281
|
-
|
|
1282
|
-
const tableData = React.useMemo(
|
|
1283
|
-
() => placementsForPhase(lifecycleTabId),
|
|
1284
|
-
[lifecycleTabId],
|
|
1285
|
-
)
|
|
1286
|
-
|
|
1287
|
-
const filterFields = React.useMemo(() => columnsToFilterFields(columns), [columns])
|
|
1288
|
-
|
|
1289
|
-
const fieldDefinitionsForDrawer = React.useMemo(
|
|
1290
|
-
() => columns
|
|
1291
|
-
.filter(c => c.key !== "select" && c.key !== "actions")
|
|
1292
|
-
.map(c => ({
|
|
1293
|
-
key: c.key,
|
|
1294
|
-
label: c.label,
|
|
1295
|
-
sortable: !!(c.sortable && c.sortKey),
|
|
1296
|
-
})),
|
|
1297
|
-
[columns],
|
|
1298
|
-
)
|
|
1299
|
-
|
|
1300
|
-
const resolveColumnLabel = React.useCallback(
|
|
1301
|
-
(key: string) => columns.find(c => c.key === key)?.label ?? key,
|
|
1302
|
-
[columns],
|
|
1303
|
-
)
|
|
1304
|
-
|
|
1305
|
-
const [conditionalRules, setConditionalRules] = React.useState<ConditionalRule[]>([])
|
|
1306
|
-
|
|
1307
|
-
function addConditionalRule(rule: Omit<ConditionalRule, "id">) {
|
|
1308
|
-
setConditionalRules(prev => [...prev, { ...rule, id: `cr-${Date.now()}` }])
|
|
1309
|
-
}
|
|
1310
|
-
function removeConditionalRule(id: string) {
|
|
1311
|
-
setConditionalRules(prev => prev.filter(r => r.id !== id))
|
|
1312
|
-
}
|
|
1313
|
-
function updateConditionalRule(id: string, patch: Partial<ConditionalRule>) {
|
|
1314
|
-
setConditionalRules(prev => prev.map(r => r.id === id ? { ...r, ...patch } : r))
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
const renderFilterOptionValue = React.useCallback(
|
|
1318
|
-
(fieldKey: string, value: string): React.ReactNode => {
|
|
1319
|
-
if (fieldKey === "status") return <StatusBadge status={value as Status} />
|
|
1320
|
-
const col = columns.find(c => c.key === fieldKey)
|
|
1321
|
-
const opt = col?.filter?.options?.find(o => o.value === value)
|
|
1322
|
-
return <span className="text-foreground">{opt?.label ?? value}</span>
|
|
1323
|
-
},
|
|
1324
|
-
[columns],
|
|
1325
|
-
)
|
|
1326
|
-
|
|
1327
|
-
const [paginationPage, setPaginationPage] = React.useState(1)
|
|
1328
|
-
const [paginationPageSize, setPaginationPageSize] = React.useState(10)
|
|
1329
|
-
const [filteredCount, setFilteredCount] = React.useState(tableData.length)
|
|
1330
|
-
|
|
1331
|
-
React.useEffect(() => {
|
|
1332
|
-
setFilteredCount(tableData.length)
|
|
1333
|
-
}, [tableData])
|
|
1334
|
-
|
|
1335
|
-
const totalPages = Math.max(1, Math.ceil(filteredCount / Math.max(1, paginationPageSize)))
|
|
1336
|
-
const safePage = Math.min(paginationPage, totalPages)
|
|
1337
|
-
const paginationOverride =
|
|
1338
|
-
pagination && view !== "board" && view !== "dashboard" && view !== "folder" && view !== "panel" && view !== "tree-panel"
|
|
1339
|
-
? { page: safePage, pageSize: paginationPageSize }
|
|
1340
|
-
: undefined
|
|
1341
|
-
|
|
1342
|
-
const tableState = useTableState(tableData, columns, { key: "student", dir: "asc" }, paginationOverride)
|
|
1343
|
-
|
|
1344
|
-
const columnKeys = React.useMemo(() => new Set(columns.map(c => c.key)), [columns])
|
|
1345
|
-
|
|
1346
|
-
// Stable "open properties drawer" callback ref — see top of this file.
|
|
1347
|
-
React.useEffect(() => {
|
|
1348
|
-
openDrawerRef.current = () => tableState.setSheetOpen(true)
|
|
1349
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1350
|
-
}, [openDrawerRef, tableState.setSheetOpen])
|
|
1351
|
-
|
|
1352
|
-
// ── Lifecycle persistence ─────────────────────────────────────────────
|
|
1353
|
-
// Centralised in `lib/table-state-lifecycle` — one hook wires both the
|
|
1354
|
-
// load (layout effect) and the debounced save (effect) including all the
|
|
1355
|
-
// table slices plus placements-specific extras. Hubs that don't want
|
|
1356
|
-
// localStorage persistence simply don't call this hook.
|
|
1357
|
-
useTableStateLifecycle<PlacementsLifecycleExtras>({
|
|
1358
|
-
namespace: PLACEMENTS_LIFECYCLE_NAMESPACE,
|
|
1359
|
-
tabId: lifecycleTabId,
|
|
1360
|
-
tableState,
|
|
1361
|
-
columnKeys,
|
|
1362
|
-
extras: {
|
|
1363
|
-
conditionalRules,
|
|
1364
|
-
pagination,
|
|
1365
|
-
paginationPage: safePage,
|
|
1366
|
-
paginationPageSize,
|
|
1367
|
-
},
|
|
1368
|
-
onLoadExtras: e => {
|
|
1369
|
-
if (!e) return
|
|
1370
|
-
if (Array.isArray(e.conditionalRules)) setConditionalRules(e.conditionalRules as ConditionalRule[])
|
|
1371
|
-
if (typeof e.pagination === "boolean") setPagination(e.pagination)
|
|
1372
|
-
if (typeof e.paginationPage === "number") setPaginationPage(e.paginationPage)
|
|
1373
|
-
if (typeof e.paginationPageSize === "number") setPaginationPageSize(e.paginationPageSize)
|
|
1374
|
-
},
|
|
1375
|
-
})
|
|
1376
|
-
|
|
1377
|
-
function buildToolbarSlot(
|
|
1378
|
-
s: ReturnType<typeof useTableState<Placement>>,
|
|
1379
|
-
): React.ReactNode {
|
|
1380
|
-
return (
|
|
1381
|
-
<TablePropertiesDrawerButton
|
|
1382
|
-
state={s}
|
|
1383
|
-
totalRows={tableData.length}
|
|
1384
|
-
pagination={pagination}
|
|
1385
|
-
onPaginationChange={setPagination}
|
|
1386
|
-
conditionalRules={conditionalRules}
|
|
1387
|
-
onAddConditionalRule={addConditionalRule}
|
|
1388
|
-
onRemoveConditionalRule={removeConditionalRule}
|
|
1389
|
-
onUpdateConditionalRule={updateConditionalRule}
|
|
1390
|
-
filterFields={filterFields}
|
|
1391
|
-
currentView={view}
|
|
1392
|
-
onViewChange={onViewChange}
|
|
1393
|
-
lifecycleTabLabel={lifecycleDrawerLabel}
|
|
1394
|
-
fieldDefinitions={fieldDefinitionsForDrawer}
|
|
1395
|
-
resolveColumnLabel={resolveColumnLabel}
|
|
1396
|
-
displayOptions={displayOptions}
|
|
1397
|
-
onDisplayOptionsChange={patchDisplayOptions}
|
|
1398
|
-
renderFilterOptionValue={renderFilterOptionValue}
|
|
1399
|
-
/>
|
|
1400
|
-
)
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
function bulkActionsSlot(selected: Set<string | number>, _rows: Placement[]): React.ReactNode {
|
|
1404
|
-
const count = selected.size
|
|
1405
|
-
const contextId = "bulk-selection-context"
|
|
1406
|
-
return (
|
|
1407
|
-
<>
|
|
1408
|
-
<span id={contextId} className="sr-only">
|
|
1409
|
-
{count} {count === 1 ? "row" : "rows"} selected
|
|
1410
|
-
</span>
|
|
1411
|
-
<Button size="sm" variant="default" aria-describedby={contextId}>
|
|
1412
|
-
<i className="fa-light fa-circle-check" aria-hidden="true" /> Confirm
|
|
1413
|
-
</Button>
|
|
1414
|
-
<Button size="sm" variant="outline" aria-describedby={contextId}>
|
|
1415
|
-
<i className="fa-light fa-arrow-down-to-line" aria-hidden="true" /> Export
|
|
1416
|
-
</Button>
|
|
1417
|
-
<Button size="sm" variant="destructive" aria-describedby={contextId}>
|
|
1418
|
-
<i className="fa-light fa-trash" aria-hidden="true" /> Delete
|
|
1419
|
-
</Button>
|
|
1420
|
-
</>
|
|
1421
|
-
)
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
const tableProps: DataTableExtendedProps<Placement> = {
|
|
1425
|
-
data: tableData,
|
|
1426
|
-
columns,
|
|
1427
|
-
getRowId: (row: Placement) => row.id,
|
|
1428
|
-
getRowSelectionLabel: (row: Placement) => row.student,
|
|
1429
|
-
selectable: true,
|
|
1430
|
-
searchable: displayOptions.showToolbarSearch,
|
|
1431
|
-
showColumnHeaders: displayOptions.showColumnLabels,
|
|
1432
|
-
defaultSort: { key: "student" as const, dir: "asc" as const },
|
|
1433
|
-
emptyState: emptyTableCopy,
|
|
1434
|
-
toolbarSlot: buildToolbarSlot,
|
|
1435
|
-
bulkActionsSlot,
|
|
1436
|
-
renderFilterOptionValue,
|
|
1437
|
-
conditionalRules,
|
|
1438
|
-
onRowClick: (row: Placement) => router.push(`/data-list/${row.id}`),
|
|
1439
|
-
state: tableState,
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
if (view === "board") {
|
|
1443
|
-
return (
|
|
1444
|
-
<DataListBoardShell
|
|
1445
|
-
state={tableState}
|
|
1446
|
-
openDrawerRef={openDrawerRef}
|
|
1447
|
-
tableData={tableData}
|
|
1448
|
-
columns={columns}
|
|
1449
|
-
lifecycleTabId={lifecycleTabId}
|
|
1450
|
-
view={view}
|
|
1451
|
-
onViewChange={onViewChange}
|
|
1452
|
-
pagination={pagination}
|
|
1453
|
-
onPaginationChange={setPagination}
|
|
1454
|
-
conditionalRules={conditionalRules}
|
|
1455
|
-
onAddConditionalRule={addConditionalRule}
|
|
1456
|
-
onRemoveConditionalRule={removeConditionalRule}
|
|
1457
|
-
onUpdateConditionalRule={updateConditionalRule}
|
|
1458
|
-
filterFields={filterFields}
|
|
1459
|
-
lifecycleDrawerLabel={lifecycleDrawerLabel}
|
|
1460
|
-
fieldDefinitionsForDrawer={fieldDefinitionsForDrawer}
|
|
1461
|
-
resolveColumnLabel={resolveColumnLabel}
|
|
1462
|
-
renderFilterOptionValue={renderFilterOptionValue}
|
|
1463
|
-
displayOptions={displayOptions}
|
|
1464
|
-
onDisplayOptionsChange={patchDisplayOptions}
|
|
1465
|
-
/>
|
|
1466
|
-
)
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
if (view === "dashboard") {
|
|
1470
|
-
return (
|
|
1471
|
-
<DataListDashboardShell
|
|
1472
|
-
state={tableState}
|
|
1473
|
-
openDrawerRef={openDrawerRef}
|
|
1474
|
-
tableData={tableData}
|
|
1475
|
-
columns={columns}
|
|
1476
|
-
view={view}
|
|
1477
|
-
onViewChange={onViewChange}
|
|
1478
|
-
pagination={pagination}
|
|
1479
|
-
onPaginationChange={setPagination}
|
|
1480
|
-
conditionalRules={conditionalRules}
|
|
1481
|
-
onAddConditionalRule={addConditionalRule}
|
|
1482
|
-
onRemoveConditionalRule={removeConditionalRule}
|
|
1483
|
-
onUpdateConditionalRule={updateConditionalRule}
|
|
1484
|
-
filterFields={filterFields}
|
|
1485
|
-
lifecycleDrawerLabel={lifecycleDrawerLabel}
|
|
1486
|
-
fieldDefinitionsForDrawer={fieldDefinitionsForDrawer}
|
|
1487
|
-
resolveColumnLabel={resolveColumnLabel}
|
|
1488
|
-
renderFilterOptionValue={renderFilterOptionValue}
|
|
1489
|
-
displayOptions={displayOptions}
|
|
1490
|
-
onDisplayOptionsChange={patchDisplayOptions}
|
|
1491
|
-
/>
|
|
1492
|
-
)
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
if (view === "list") {
|
|
1496
|
-
return (
|
|
1497
|
-
<React.Fragment key={lifecycleTabId}>
|
|
1498
|
-
{pagination ? (
|
|
1499
|
-
<CountSyncer
|
|
1500
|
-
count={tableState.rows.length}
|
|
1501
|
-
onSync={setFilteredCount}
|
|
1502
|
-
onReset={() => setPaginationPage(1)}
|
|
1503
|
-
/>
|
|
1504
|
-
) : null}
|
|
1505
|
-
<DataListListShell
|
|
1506
|
-
state={tableState}
|
|
1507
|
-
openDrawerRef={openDrawerRef}
|
|
1508
|
-
tableData={tableData}
|
|
1509
|
-
columns={columns}
|
|
1510
|
-
lifecycleTabId={lifecycleTabId}
|
|
1511
|
-
view={view}
|
|
1512
|
-
onViewChange={onViewChange}
|
|
1513
|
-
pagination={pagination}
|
|
1514
|
-
onPaginationChange={setPagination}
|
|
1515
|
-
conditionalRules={conditionalRules}
|
|
1516
|
-
onAddConditionalRule={addConditionalRule}
|
|
1517
|
-
onRemoveConditionalRule={removeConditionalRule}
|
|
1518
|
-
onUpdateConditionalRule={updateConditionalRule}
|
|
1519
|
-
filterFields={filterFields}
|
|
1520
|
-
lifecycleDrawerLabel={lifecycleDrawerLabel}
|
|
1521
|
-
fieldDefinitionsForDrawer={fieldDefinitionsForDrawer}
|
|
1522
|
-
resolveColumnLabel={resolveColumnLabel}
|
|
1523
|
-
renderFilterOptionValue={renderFilterOptionValue}
|
|
1524
|
-
displayOptions={displayOptions}
|
|
1525
|
-
onDisplayOptionsChange={patchDisplayOptions}
|
|
1526
|
-
listRows={pagination ? tableState.pagedRows : tableState.rows}
|
|
1527
|
-
emptyTableCopy={emptyTableCopy}
|
|
1528
|
-
/>
|
|
1529
|
-
{pagination ? (
|
|
1530
|
-
<div className="mx-4 lg:mx-6 border-x border-b border-border rounded-b-lg overflow-hidden">
|
|
1531
|
-
<PaginationBar
|
|
1532
|
-
page={safePage}
|
|
1533
|
-
pageSize={paginationPageSize}
|
|
1534
|
-
total={filteredCount}
|
|
1535
|
-
pageSizeOptions={[10, 25, 50, 100]}
|
|
1536
|
-
onPageChange={setPaginationPage}
|
|
1537
|
-
onPageSizeChange={n => {
|
|
1538
|
-
setPaginationPageSize(n)
|
|
1539
|
-
setPaginationPage(1)
|
|
1540
|
-
}}
|
|
1541
|
-
/>
|
|
1542
|
-
</div>
|
|
1543
|
-
) : null}
|
|
1544
|
-
</React.Fragment>
|
|
1545
|
-
)
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
if (view === "folder") {
|
|
1549
|
-
return (
|
|
1550
|
-
<DataListFolderShell
|
|
1551
|
-
key={lifecycleTabId}
|
|
1552
|
-
state={tableState}
|
|
1553
|
-
openDrawerRef={openDrawerRef}
|
|
1554
|
-
tableData={tableData}
|
|
1555
|
-
columns={columns}
|
|
1556
|
-
lifecycleTabId={lifecycleTabId}
|
|
1557
|
-
view={view}
|
|
1558
|
-
onViewChange={onViewChange}
|
|
1559
|
-
pagination={pagination}
|
|
1560
|
-
onPaginationChange={setPagination}
|
|
1561
|
-
conditionalRules={conditionalRules}
|
|
1562
|
-
onAddConditionalRule={addConditionalRule}
|
|
1563
|
-
onRemoveConditionalRule={removeConditionalRule}
|
|
1564
|
-
onUpdateConditionalRule={updateConditionalRule}
|
|
1565
|
-
filterFields={filterFields}
|
|
1566
|
-
lifecycleDrawerLabel={lifecycleDrawerLabel}
|
|
1567
|
-
fieldDefinitionsForDrawer={fieldDefinitionsForDrawer}
|
|
1568
|
-
resolveColumnLabel={resolveColumnLabel}
|
|
1569
|
-
renderFilterOptionValue={renderFilterOptionValue}
|
|
1570
|
-
displayOptions={displayOptions}
|
|
1571
|
-
onDisplayOptionsChange={patchDisplayOptions}
|
|
1572
|
-
listRows={tableState.rows}
|
|
1573
|
-
emptyTableCopy={emptyTableCopy}
|
|
1574
|
-
/>
|
|
1575
|
-
)
|
|
1576
|
-
}
|
|
1577
|
-
|
|
1578
|
-
if (view === "tree-panel") {
|
|
1579
|
-
return (
|
|
1580
|
-
<DataListTreeShell
|
|
1581
|
-
key={lifecycleTabId}
|
|
1582
|
-
state={tableState}
|
|
1583
|
-
openDrawerRef={openDrawerRef}
|
|
1584
|
-
tableData={tableData}
|
|
1585
|
-
columns={columns}
|
|
1586
|
-
lifecycleTabId={lifecycleTabId}
|
|
1587
|
-
view={view}
|
|
1588
|
-
onViewChange={onViewChange}
|
|
1589
|
-
pagination={pagination}
|
|
1590
|
-
onPaginationChange={setPagination}
|
|
1591
|
-
conditionalRules={conditionalRules}
|
|
1592
|
-
onAddConditionalRule={addConditionalRule}
|
|
1593
|
-
onRemoveConditionalRule={removeConditionalRule}
|
|
1594
|
-
onUpdateConditionalRule={updateConditionalRule}
|
|
1595
|
-
filterFields={filterFields}
|
|
1596
|
-
lifecycleDrawerLabel={lifecycleDrawerLabel}
|
|
1597
|
-
fieldDefinitionsForDrawer={fieldDefinitionsForDrawer}
|
|
1598
|
-
resolveColumnLabel={resolveColumnLabel}
|
|
1599
|
-
renderFilterOptionValue={renderFilterOptionValue}
|
|
1600
|
-
displayOptions={displayOptions}
|
|
1601
|
-
onDisplayOptionsChange={patchDisplayOptions}
|
|
1602
|
-
listRows={tableState.rows}
|
|
1603
|
-
emptyTableCopy={emptyTableCopy}
|
|
1604
|
-
/>
|
|
1605
|
-
)
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
if (view === "panel") {
|
|
1609
|
-
return (
|
|
1610
|
-
<DataListPanelShell
|
|
1611
|
-
key={lifecycleTabId}
|
|
1612
|
-
state={tableState}
|
|
1613
|
-
openDrawerRef={openDrawerRef}
|
|
1614
|
-
tableData={tableData}
|
|
1615
|
-
columns={columns}
|
|
1616
|
-
lifecycleTabId={lifecycleTabId}
|
|
1617
|
-
view={view}
|
|
1618
|
-
onViewChange={onViewChange}
|
|
1619
|
-
pagination={pagination}
|
|
1620
|
-
onPaginationChange={setPagination}
|
|
1621
|
-
conditionalRules={conditionalRules}
|
|
1622
|
-
onAddConditionalRule={addConditionalRule}
|
|
1623
|
-
onRemoveConditionalRule={removeConditionalRule}
|
|
1624
|
-
onUpdateConditionalRule={updateConditionalRule}
|
|
1625
|
-
filterFields={filterFields}
|
|
1626
|
-
lifecycleDrawerLabel={lifecycleDrawerLabel}
|
|
1627
|
-
fieldDefinitionsForDrawer={fieldDefinitionsForDrawer}
|
|
1628
|
-
resolveColumnLabel={resolveColumnLabel}
|
|
1629
|
-
renderFilterOptionValue={renderFilterOptionValue}
|
|
1630
|
-
displayOptions={displayOptions}
|
|
1631
|
-
onDisplayOptionsChange={patchDisplayOptions}
|
|
1632
|
-
listRows={tableState.rows}
|
|
1633
|
-
emptyTableCopy={emptyTableCopy}
|
|
1634
|
-
panelGroupsBuilder={panelGroupsBuilder}
|
|
1635
|
-
panelRenderListRow={panelRenderListRow}
|
|
1636
|
-
panelRenderDetail={panelRenderDetail}
|
|
1637
|
-
/>
|
|
1638
|
-
)
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
if (pagination) {
|
|
1642
|
-
return (
|
|
1643
|
-
<React.Fragment key={lifecycleTabId}>
|
|
1644
|
-
<CountSyncer
|
|
1645
|
-
count={tableState.rows.length}
|
|
1646
|
-
onSync={setFilteredCount}
|
|
1647
|
-
onReset={() => setPaginationPage(1)}
|
|
1648
|
-
/>
|
|
1649
|
-
<DataTable<Placement> {...tableProps} hasFooter />
|
|
1650
|
-
<div className="mx-4 lg:mx-6 border-x border-b border-border rounded-b-lg overflow-hidden">
|
|
1651
|
-
<PaginationBar
|
|
1652
|
-
page={safePage}
|
|
1653
|
-
pageSize={paginationPageSize}
|
|
1654
|
-
total={filteredCount}
|
|
1655
|
-
pageSizeOptions={[10, 25, 50, 100]}
|
|
1656
|
-
onPageChange={setPaginationPage}
|
|
1657
|
-
onPageSizeChange={n => {
|
|
1658
|
-
setPaginationPageSize(n)
|
|
1659
|
-
setPaginationPage(1)
|
|
1660
|
-
}}
|
|
1661
|
-
/>
|
|
1662
|
-
</div>
|
|
1663
|
-
</React.Fragment>
|
|
1664
|
-
)
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
return <DataTable<Placement> key={lifecycleTabId} {...tableProps} />
|
|
1668
|
-
})
|
|
1669
|
-
|
|
1670
|
-
PlacementsTable.displayName = "PlacementsTable"
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
export type { DataListViewType } from "@/lib/data-list-view"
|
|
1674
|
-
export { DATA_LIST_VIEW_TILES } from "@/lib/data-list-view"
|
|
1675
|
-
export type { DataListDisplayOptions } from "@/lib/data-list-display-options"
|