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