@exxatdesignux/ui 0.2.6 → 0.2.7
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/package.json +2 -1
- package/template/.agents/skills/shadcn/SKILL.md +242 -0
- package/template/.agents/skills/shadcn/agents/openai.yml +5 -0
- package/template/.agents/skills/shadcn/assets/shadcn-small.png +0 -0
- package/template/.agents/skills/shadcn/assets/shadcn.png +0 -0
- package/template/.agents/skills/shadcn/cli.md +257 -0
- package/template/.agents/skills/shadcn/customization.md +202 -0
- package/template/.agents/skills/shadcn/evals/evals.json +47 -0
- package/template/.agents/skills/shadcn/mcp.md +94 -0
- package/template/.agents/skills/shadcn/rules/base-vs-radix.md +306 -0
- package/template/.agents/skills/shadcn/rules/composition.md +195 -0
- package/template/.agents/skills/shadcn/rules/forms.md +192 -0
- package/template/.agents/skills/shadcn/rules/icons.md +101 -0
- package/template/.agents/skills/shadcn/rules/styling.md +162 -0
- package/template/.claude/skills/exxat-ds-skill/SKILL.md +712 -0
- package/template/.cursor/rules/exxat-accessibility.mdc +33 -0
- package/template/.cursor/rules/exxat-command-menu.mdc +23 -0
- package/template/.cursor/rules/exxat-dashboard-view-charts.mdc +53 -0
- package/template/.cursor/rules/exxat-data-tables.mdc +31 -0
- package/template/.cursor/rules/exxat-ds-agents.mdc +26 -0
- package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +100 -0
- package/template/.cursor/rules/exxat-list-page-connected-views.mdc +16 -0
- package/template/.cursor/rules/exxat-no-toast.mdc +26 -0
- package/template/.cursor/rules/exxat-page-vs-drawer.mdc +22 -0
- package/template/.cursor/rules/exxat-table-properties-drawer.mdc +40 -0
- package/template/AGENTS.md +52 -11
- package/template/app/(app)/dashboard/page.tsx +1 -1
- package/template/app/(app)/data-list/[id]/page.tsx +24 -8
- package/template/app/(app)/data-list/new/page.tsx +7 -4
- package/template/app/(app)/data-list/page.tsx +1 -1
- package/template/app/(app)/examples/page.tsx +41 -0
- package/template/app/(app)/question-bank/page.tsx +3 -3
- package/template/app/globals.css +1 -1
- package/template/components/app-sidebar.tsx +52 -35
- package/template/components/compliance-table.tsx +79 -0
- package/template/components/data-list-client.tsx +36 -25
- package/template/components/data-list-table.tsx +797 -10
- package/template/components/data-views/finder-panel-view.tsx +405 -0
- package/template/components/data-views/folder-grid-view.tsx +86 -0
- package/template/components/data-views/index.ts +59 -0
- package/template/components/data-views/list-page-split-details-placeholder.tsx +39 -0
- package/template/components/data-views/list-page-split-hub-chrome.tsx +60 -0
- package/template/components/data-views/list-page-split-hub-tokens.ts +16 -0
- package/template/components/data-views/list-page-tree-column-header.tsx +31 -0
- package/template/components/data-views/list-page-tree-panel-shell.tsx +91 -0
- package/template/components/data-views/list-page-view-frame.tsx +53 -0
- package/template/components/data-views/os-folder-glyph.tsx +121 -0
- package/template/components/folder-details-shell.tsx +230 -0
- package/template/components/hub-tree-panel-view.tsx +672 -0
- package/template/components/list-hub-status-badge.tsx +17 -3
- package/template/components/placements-page-header.tsx +14 -8
- package/template/components/placements-table-columns.tsx +8 -8
- package/template/components/question-bank-client.tsx +157 -40
- package/template/components/question-bank-new-folder-sheet.tsx +248 -0
- package/template/components/question-bank-os-folder-view.tsx +648 -0
- package/template/components/question-bank-page-header.tsx +3 -3
- package/template/components/question-bank-panel-activator.tsx +9 -0
- package/template/components/question-bank-secondary-nav.tsx +226 -0
- package/template/components/question-bank-table.tsx +707 -22
- package/template/components/secondary-panel.tsx +41 -107
- package/template/components/sites-table.tsx +66 -0
- package/template/components/team-client.tsx +7 -0
- package/template/components/team-table.tsx +156 -1
- package/template/components/templates/list-page.tsx +2 -2
- package/template/components/ui/avatar.tsx +1 -1
- package/template/components/ui/badge.tsx +1 -1
- package/template/components/ui/banner.tsx +1 -1
- package/template/components/ui/breadcrumb.tsx +1 -1
- package/template/components/ui/button.tsx +1 -1
- package/template/components/ui/calendar.tsx +1 -1
- package/template/components/ui/card.tsx +1 -1
- package/template/components/ui/chart.tsx +1 -1
- package/template/components/ui/checkbox.tsx +1 -1
- package/template/components/ui/coach-mark.tsx +1 -1
- package/template/components/ui/collapsible.tsx +1 -1
- package/template/components/ui/command.tsx +1 -1
- package/template/components/ui/date-picker-field.tsx +1 -1
- package/template/components/ui/dialog.tsx +1 -1
- package/template/components/ui/drag-handle-grip.tsx +1 -1
- package/template/components/ui/drawer.tsx +1 -1
- package/template/components/ui/dropdown-menu.tsx +1 -1
- package/template/components/ui/field.tsx +1 -1
- package/template/components/ui/form.tsx +1 -1
- package/template/components/ui/input-group.tsx +1 -1
- package/template/components/ui/input-mask.tsx +1 -1
- package/template/components/ui/input.tsx +1 -1
- package/template/components/ui/kbd.tsx +1 -1
- package/template/components/ui/label.tsx +1 -1
- package/template/components/ui/payment-card-fields.tsx +1 -1
- package/template/components/ui/popover.tsx +1 -1
- package/template/components/ui/radio-group.tsx +1 -1
- package/template/components/ui/resizable.tsx +68 -0
- package/template/components/ui/select.tsx +1 -1
- package/template/components/ui/selection-tile-grid.tsx +1 -1
- package/template/components/ui/separator.tsx +1 -1
- package/template/components/ui/sheet.tsx +1 -1
- package/template/components/ui/sidebar.tsx +1 -1
- package/template/components/ui/skeleton.tsx +1 -1
- package/template/components/ui/sonner.tsx +1 -1
- package/template/components/ui/status-badge.tsx +1 -1
- package/template/components/ui/table.tsx +1 -1
- package/template/components/ui/tabs.tsx +1 -1
- package/template/components/ui/textarea.tsx +1 -1
- package/template/components/ui/tip.tsx +1 -1
- package/template/components/ui/toggle-group.tsx +1 -1
- package/template/components/ui/toggle-switch.tsx +1 -1
- package/template/components/ui/toggle.tsx +1 -1
- package/template/components/ui/tooltip.tsx +1 -1
- package/template/components/ui/view-segmented-control.tsx +1 -1
- package/template/docs/data-views-pattern.md +7 -0
- package/template/hooks/use-app-theme.ts +1 -1
- package/template/hooks/use-coach-mark.ts +1 -1
- package/template/hooks/use-location-hash.ts +15 -0
- package/template/hooks/use-mobile.ts +1 -1
- package/template/hooks/use-mod-key-label.ts +1 -1
- package/template/hooks/use-sidebar-reflow-zoom.ts +40 -0
- package/template/lib/ask-leo-route-context.ts +25 -57
- package/template/lib/coach-mark-registry.ts +13 -13
- package/template/lib/command-menu-config.ts +28 -23
- package/template/lib/command-menu-search-data.ts +10 -9
- package/template/lib/data-list-view-surface.ts +12 -1
- package/template/lib/data-list-view.ts +6 -3
- package/template/lib/date-filter.ts +1 -1
- package/template/lib/mock/dashboard.ts +11 -11
- package/template/lib/mock/navigation.tsx +22 -63
- package/template/lib/mock/placements-kpi.ts +19 -19
- package/template/lib/mock/question-bank-folders.ts +167 -0
- package/template/lib/mock/question-bank-inspector.ts +109 -0
- package/template/lib/mock/question-bank-kpi.ts +1 -1
- package/template/lib/mock/question-bank.ts +80 -0
- package/template/lib/question-bank-nav.ts +91 -0
- package/template/lib/utils.ts +1 -1
- package/template/next.config.mjs +8 -0
- package/template/package.json +1 -0
- package/template/public/folders/icons8-folder-windows-11.svg +1 -0
- package/template/app/(app)/compliance/page.tsx +0 -10
- package/template/app/(app)/rotations/page.tsx +0 -15
- package/template/app/(app)/sites/all/page.tsx +0 -13
- package/template/app/(app)/team/page.tsx +0 -10
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* DataListTable —
|
|
4
|
+
* DataListTable — list hub shell on top of the generic DataTable.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* View tabs drive `view` (table | list | board | …). `lifecycleTabId` selects which **demo row
|
|
7
|
+
* segment** (columns + filtered rows) to use — keep in sync with each tab's `filterId`, or pass
|
|
8
|
+
* `"all"` for tabs that only change layout.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import * as React from "react"
|
|
11
12
|
import dynamic from "next/dynamic"
|
|
13
|
+
import { cn } from "@/lib/utils"
|
|
12
14
|
import { useRouter } from "next/navigation"
|
|
13
15
|
import { Button } from "@/components/ui/button"
|
|
14
16
|
import { Tip } from "@/components/ui/tip"
|
|
@@ -30,6 +32,15 @@ import { useCoachMark } from "@/hooks/use-coach-mark"
|
|
|
30
32
|
import { DASHBOARD_CUSTOMIZE_COACH_STEPS } from "@/lib/dashboard-customize-coach-mark"
|
|
31
33
|
import { PlacementsBoardView, type PlacementsBoardColumnMenu } from "@/components/placements-board-view"
|
|
32
34
|
import { PlacementsListView } from "@/components/placements-list-view"
|
|
35
|
+
import { FolderGridView, ListPageTreePanelShell } from "@/components/data-views"
|
|
36
|
+
import { ListPageSplitHubChrome } from "@/components/data-views/list-page-split-hub-chrome"
|
|
37
|
+
import { ListPageTreeColumnHeader } from "@/components/data-views/list-page-tree-column-header"
|
|
38
|
+
import { FinderPanelView, type FinderGroup } from "@/components/data-views/finder-panel-view"
|
|
39
|
+
import { ListPageSplitDetailsPlaceholder } from "@/components/data-views/list-page-split-details-placeholder"
|
|
40
|
+
import { AvatarInitials } from "@/components/ui/avatar"
|
|
41
|
+
import { getConditionalRowBackground } from "@/lib/conditional-rule-match"
|
|
42
|
+
import { isBoardFieldActive } from "@/lib/placement-board-card-layout"
|
|
43
|
+
import type { BoardCardLifecycleTabId } from "@/lib/placement-board-card-layout"
|
|
33
44
|
import { TablePropertiesDrawerButton } from "@/components/table-properties"
|
|
34
45
|
import type { FilterFieldDef } from "@/components/table-properties/types"
|
|
35
46
|
import type { DataListViewType } from "@/lib/data-list-view"
|
|
@@ -148,7 +159,7 @@ function DataListBoardShell({
|
|
|
148
159
|
columns={columns}
|
|
149
160
|
searchable
|
|
150
161
|
renderFilterOptionValue={renderFilterOptionValue}
|
|
151
|
-
searchAriaLabel="Search
|
|
162
|
+
searchAriaLabel="Search rows"
|
|
152
163
|
toolbarSlot={(s) => (
|
|
153
164
|
<TablePropertiesDrawerButton
|
|
154
165
|
state={s}
|
|
@@ -248,7 +259,7 @@ function DataListListShell({
|
|
|
248
259
|
columns={columns}
|
|
249
260
|
searchable
|
|
250
261
|
renderFilterOptionValue={renderFilterOptionValue}
|
|
251
|
-
searchAriaLabel="Search
|
|
262
|
+
searchAriaLabel="Search rows"
|
|
252
263
|
toolbarSlot={s => (
|
|
253
264
|
<TablePropertiesDrawerButton
|
|
254
265
|
state={s}
|
|
@@ -421,10 +432,10 @@ function DataListDashboardShell({
|
|
|
421
432
|
}, [])
|
|
422
433
|
|
|
423
434
|
const dashboardCustomizeCoach = useCoachMark({
|
|
424
|
-
flowId: "
|
|
435
|
+
flowId: "data-list-dashboard-customize",
|
|
425
436
|
steps: DASHBOARD_CUSTOMIZE_COACH_STEPS,
|
|
426
437
|
delay: 700,
|
|
427
|
-
dependsOnDismissedFlowId: "
|
|
438
|
+
dependsOnDismissedFlowId: "data-list-views-tour",
|
|
428
439
|
})
|
|
429
440
|
|
|
430
441
|
return (
|
|
@@ -436,7 +447,7 @@ function DataListDashboardShell({
|
|
|
436
447
|
columns={columns}
|
|
437
448
|
searchable={displayOptions.showToolbarSearch}
|
|
438
449
|
renderFilterOptionValue={renderFilterOptionValue}
|
|
439
|
-
searchAriaLabel="Search
|
|
450
|
+
searchAriaLabel="Search rows"
|
|
440
451
|
toolbarSlot={s => (
|
|
441
452
|
<TablePropertiesDrawerButton
|
|
442
453
|
state={s}
|
|
@@ -498,6 +509,680 @@ function DataListDashboardShell({
|
|
|
498
509
|
)
|
|
499
510
|
}
|
|
500
511
|
|
|
512
|
+
// ─── Placement-specific tile for FolderGridView ──────────────────────────────
|
|
513
|
+
|
|
514
|
+
function PlacementFolderTile({
|
|
515
|
+
row,
|
|
516
|
+
tab,
|
|
517
|
+
hiddenColKeys,
|
|
518
|
+
boardColumns,
|
|
519
|
+
conditionalRules,
|
|
520
|
+
onClick,
|
|
521
|
+
}: {
|
|
522
|
+
row: Placement
|
|
523
|
+
tab: BoardCardLifecycleTabId
|
|
524
|
+
hiddenColKeys: Set<string>
|
|
525
|
+
boardColumns: ColumnDef<Placement>[]
|
|
526
|
+
conditionalRules?: ConditionalRule[]
|
|
527
|
+
onClick: () => void
|
|
528
|
+
}) {
|
|
529
|
+
const ruleBg = getConditionalRowBackground(row, conditionalRules)
|
|
530
|
+
const showStudent = isBoardFieldActive("student", tab, hiddenColKeys, boardColumns)
|
|
531
|
+
const showStatus = isBoardFieldActive("status", tab, hiddenColKeys, boardColumns)
|
|
532
|
+
const showSite = isBoardFieldActive("site", tab, hiddenColKeys, boardColumns)
|
|
533
|
+
const showSpec = isBoardFieldActive("specialization", tab, hiddenColKeys, boardColumns)
|
|
534
|
+
const showProgram = isBoardFieldActive("program", tab, hiddenColKeys, boardColumns)
|
|
535
|
+
const name = showStudent ? row.student : `Placement ${row.id}`
|
|
536
|
+
|
|
537
|
+
const statusDotClass: Record<Status, string> = {
|
|
538
|
+
confirmed: "bg-success",
|
|
539
|
+
pending: "bg-warning",
|
|
540
|
+
"under-review": "bg-brand",
|
|
541
|
+
completed: "bg-muted-foreground",
|
|
542
|
+
rejected: "bg-destructive",
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return (
|
|
546
|
+
<button
|
|
547
|
+
type="button"
|
|
548
|
+
onClick={onClick}
|
|
549
|
+
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}`}
|
|
550
|
+
aria-label={`Open ${name}`}
|
|
551
|
+
>
|
|
552
|
+
<div className="relative">
|
|
553
|
+
<AvatarInitials initials={row.initials} className="size-14 rounded-full text-lg font-semibold" />
|
|
554
|
+
{showStatus && (
|
|
555
|
+
<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">
|
|
556
|
+
<span className={`size-2.5 rounded-full ${statusDotClass[row.status]}`} />
|
|
557
|
+
</span>
|
|
558
|
+
)}
|
|
559
|
+
</div>
|
|
560
|
+
<p className="w-full text-center text-[13px] font-medium text-foreground leading-tight line-clamp-2">{name}</p>
|
|
561
|
+
{showStatus && <StatusBadge status={row.status} />}
|
|
562
|
+
{(showSite || showSpec || showProgram) && (
|
|
563
|
+
<div className="flex w-full flex-col gap-0.5">
|
|
564
|
+
{showSite && (
|
|
565
|
+
<p className="truncate text-center text-[11px] text-muted-foreground leading-tight">
|
|
566
|
+
<i className="fa-light fa-building mr-1" aria-hidden="true" />{row.site}
|
|
567
|
+
</p>
|
|
568
|
+
)}
|
|
569
|
+
{showSpec && <p className="truncate text-center text-[11px] text-muted-foreground leading-tight">{row.specialization}</p>}
|
|
570
|
+
{showProgram && <p className="truncate text-center text-[11px] text-muted-foreground leading-tight">{row.program}</p>}
|
|
571
|
+
</div>
|
|
572
|
+
)}
|
|
573
|
+
</button>
|
|
574
|
+
)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
// ─── Folder view shell ────────────────────────────────────────────────────────
|
|
579
|
+
|
|
580
|
+
/** Folder / icon-grid view shell */
|
|
581
|
+
function DataListFolderShell({
|
|
582
|
+
state,
|
|
583
|
+
openDrawerRef,
|
|
584
|
+
tableData,
|
|
585
|
+
columns,
|
|
586
|
+
lifecycleTabId,
|
|
587
|
+
view,
|
|
588
|
+
onViewChange,
|
|
589
|
+
pagination,
|
|
590
|
+
onPaginationChange,
|
|
591
|
+
conditionalRules,
|
|
592
|
+
onAddConditionalRule,
|
|
593
|
+
onRemoveConditionalRule,
|
|
594
|
+
onUpdateConditionalRule,
|
|
595
|
+
filterFields,
|
|
596
|
+
lifecycleDrawerLabel,
|
|
597
|
+
fieldDefinitionsForDrawer,
|
|
598
|
+
resolveColumnLabel,
|
|
599
|
+
renderFilterOptionValue,
|
|
600
|
+
displayOptions,
|
|
601
|
+
onDisplayOptionsChange,
|
|
602
|
+
listRows,
|
|
603
|
+
emptyTableCopy,
|
|
604
|
+
}: {
|
|
605
|
+
state: ReturnType<typeof useTableState<Placement>>
|
|
606
|
+
openDrawerRef: React.MutableRefObject<() => void>
|
|
607
|
+
tableData: Placement[]
|
|
608
|
+
columns: ColumnDef<Placement>[]
|
|
609
|
+
lifecycleTabId: PlacementLifecycleTabId
|
|
610
|
+
view: DataListViewType
|
|
611
|
+
onViewChange?: (view: DataListViewType) => void
|
|
612
|
+
pagination: boolean
|
|
613
|
+
onPaginationChange: (v: boolean) => void
|
|
614
|
+
conditionalRules: ConditionalRule[]
|
|
615
|
+
onAddConditionalRule: (rule: Omit<ConditionalRule, "id">) => void
|
|
616
|
+
onRemoveConditionalRule: (id: string) => void
|
|
617
|
+
onUpdateConditionalRule: (id: string, patch: Partial<ConditionalRule>) => void
|
|
618
|
+
filterFields: FilterFieldDef[]
|
|
619
|
+
lifecycleDrawerLabel: string
|
|
620
|
+
fieldDefinitionsForDrawer: { key: string; label: string; sortable?: boolean }[]
|
|
621
|
+
resolveColumnLabel: (key: string) => string
|
|
622
|
+
renderFilterOptionValue: (fieldKey: string, value: string) => React.ReactNode
|
|
623
|
+
displayOptions: DataListDisplayOptions
|
|
624
|
+
onDisplayOptionsChange: (patch: Partial<DataListDisplayOptions>) => void
|
|
625
|
+
listRows: Placement[]
|
|
626
|
+
emptyTableCopy: string
|
|
627
|
+
}) {
|
|
628
|
+
const router = useRouter()
|
|
629
|
+
|
|
630
|
+
React.useEffect(() => {
|
|
631
|
+
openDrawerRef.current = () => state.setSheetOpen(true)
|
|
632
|
+
}, [state.setSheetOpen])
|
|
633
|
+
|
|
634
|
+
const boardColumns = state.displayCols.filter(c => c.key !== "select" && c.key !== "actions")
|
|
635
|
+
|
|
636
|
+
return (
|
|
637
|
+
<>
|
|
638
|
+
<DataTableToolbar
|
|
639
|
+
state={state}
|
|
640
|
+
columns={columns}
|
|
641
|
+
searchable
|
|
642
|
+
renderFilterOptionValue={renderFilterOptionValue}
|
|
643
|
+
searchAriaLabel="Search rows"
|
|
644
|
+
toolbarSlot={s => (
|
|
645
|
+
<TablePropertiesDrawerButton
|
|
646
|
+
state={s}
|
|
647
|
+
totalRows={tableData.length}
|
|
648
|
+
pagination={pagination}
|
|
649
|
+
onPaginationChange={onPaginationChange}
|
|
650
|
+
conditionalRules={conditionalRules}
|
|
651
|
+
onAddConditionalRule={onAddConditionalRule}
|
|
652
|
+
onRemoveConditionalRule={onRemoveConditionalRule}
|
|
653
|
+
onUpdateConditionalRule={onUpdateConditionalRule}
|
|
654
|
+
filterFields={filterFields}
|
|
655
|
+
currentView={view}
|
|
656
|
+
onViewChange={onViewChange}
|
|
657
|
+
lifecycleTabLabel={lifecycleDrawerLabel}
|
|
658
|
+
fieldDefinitions={fieldDefinitionsForDrawer}
|
|
659
|
+
resolveColumnLabel={resolveColumnLabel}
|
|
660
|
+
displayOptions={displayOptions}
|
|
661
|
+
onDisplayOptionsChange={onDisplayOptionsChange}
|
|
662
|
+
renderFilterOptionValue={renderFilterOptionValue}
|
|
663
|
+
/>
|
|
664
|
+
)}
|
|
665
|
+
/>
|
|
666
|
+
<FolderGridView<Placement>
|
|
667
|
+
rows={listRows}
|
|
668
|
+
getRowId={r => r.id}
|
|
669
|
+
ariaLabel="Demo folder view"
|
|
670
|
+
emptyContent={<p>{emptyTableCopy}</p>}
|
|
671
|
+
renderTile={row => (
|
|
672
|
+
<PlacementFolderTile
|
|
673
|
+
row={row}
|
|
674
|
+
tab={lifecycleTabId as BoardCardLifecycleTabId}
|
|
675
|
+
hiddenColKeys={state.hiddenCols}
|
|
676
|
+
boardColumns={boardColumns}
|
|
677
|
+
conditionalRules={conditionalRules}
|
|
678
|
+
onClick={() => router.push(`/data-list/${row.id}`)}
|
|
679
|
+
/>
|
|
680
|
+
)}
|
|
681
|
+
/>
|
|
682
|
+
</>
|
|
683
|
+
)
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// ─── Tree / outline + details shell ───────────────────────────────────────────
|
|
687
|
+
|
|
688
|
+
function DataListTreeShell({
|
|
689
|
+
state,
|
|
690
|
+
openDrawerRef,
|
|
691
|
+
tableData,
|
|
692
|
+
columns,
|
|
693
|
+
lifecycleTabId,
|
|
694
|
+
view,
|
|
695
|
+
onViewChange,
|
|
696
|
+
pagination,
|
|
697
|
+
onPaginationChange,
|
|
698
|
+
conditionalRules,
|
|
699
|
+
onAddConditionalRule,
|
|
700
|
+
onRemoveConditionalRule,
|
|
701
|
+
onUpdateConditionalRule,
|
|
702
|
+
filterFields,
|
|
703
|
+
lifecycleDrawerLabel,
|
|
704
|
+
fieldDefinitionsForDrawer,
|
|
705
|
+
resolveColumnLabel,
|
|
706
|
+
renderFilterOptionValue,
|
|
707
|
+
displayOptions,
|
|
708
|
+
onDisplayOptionsChange,
|
|
709
|
+
listRows,
|
|
710
|
+
emptyTableCopy,
|
|
711
|
+
}: {
|
|
712
|
+
state: ReturnType<typeof useTableState<Placement>>
|
|
713
|
+
openDrawerRef: React.MutableRefObject<() => void>
|
|
714
|
+
tableData: Placement[]
|
|
715
|
+
columns: ColumnDef<Placement>[]
|
|
716
|
+
lifecycleTabId: PlacementLifecycleTabId
|
|
717
|
+
view: DataListViewType
|
|
718
|
+
onViewChange?: (view: DataListViewType) => void
|
|
719
|
+
pagination: boolean
|
|
720
|
+
onPaginationChange: (v: boolean) => void
|
|
721
|
+
conditionalRules: ConditionalRule[]
|
|
722
|
+
onAddConditionalRule: (rule: Omit<ConditionalRule, "id">) => void
|
|
723
|
+
onRemoveConditionalRule: (id: string) => void
|
|
724
|
+
onUpdateConditionalRule: (id: string, patch: Partial<ConditionalRule>) => void
|
|
725
|
+
filterFields: FilterFieldDef[]
|
|
726
|
+
lifecycleDrawerLabel: string
|
|
727
|
+
fieldDefinitionsForDrawer: { key: string; label: string; sortable?: boolean }[]
|
|
728
|
+
resolveColumnLabel: (key: string) => string
|
|
729
|
+
renderFilterOptionValue: (fieldKey: string, value: string) => React.ReactNode
|
|
730
|
+
displayOptions: DataListDisplayOptions
|
|
731
|
+
onDisplayOptionsChange: (patch: Partial<DataListDisplayOptions>) => void
|
|
732
|
+
listRows: Placement[]
|
|
733
|
+
emptyTableCopy: string
|
|
734
|
+
}) {
|
|
735
|
+
const boardColumns = state.displayCols.filter(c => c.key !== "select" && c.key !== "actions")
|
|
736
|
+
const [selectedId, setSelectedId] = React.useState<number | null>(() => listRows[0]?.id ?? null)
|
|
737
|
+
|
|
738
|
+
React.useEffect(() => {
|
|
739
|
+
openDrawerRef.current = () => state.setSheetOpen(true)
|
|
740
|
+
}, [state.setSheetOpen])
|
|
741
|
+
|
|
742
|
+
React.useEffect(() => {
|
|
743
|
+
if (selectedId == null) {
|
|
744
|
+
setSelectedId(listRows[0]?.id ?? null)
|
|
745
|
+
return
|
|
746
|
+
}
|
|
747
|
+
if (!listRows.some(r => r.id === selectedId)) {
|
|
748
|
+
setSelectedId(listRows[0]?.id ?? null)
|
|
749
|
+
}
|
|
750
|
+
}, [listRows, selectedId])
|
|
751
|
+
|
|
752
|
+
const selected = listRows.find(r => r.id === selectedId) ?? null
|
|
753
|
+
|
|
754
|
+
return (
|
|
755
|
+
<>
|
|
756
|
+
<DataTableToolbar
|
|
757
|
+
state={state}
|
|
758
|
+
columns={columns}
|
|
759
|
+
searchable
|
|
760
|
+
renderFilterOptionValue={renderFilterOptionValue}
|
|
761
|
+
searchAriaLabel="Search rows"
|
|
762
|
+
toolbarSlot={s => (
|
|
763
|
+
<TablePropertiesDrawerButton
|
|
764
|
+
state={s}
|
|
765
|
+
totalRows={tableData.length}
|
|
766
|
+
pagination={pagination}
|
|
767
|
+
onPaginationChange={onPaginationChange}
|
|
768
|
+
conditionalRules={conditionalRules}
|
|
769
|
+
onAddConditionalRule={onAddConditionalRule}
|
|
770
|
+
onRemoveConditionalRule={onRemoveConditionalRule}
|
|
771
|
+
onUpdateConditionalRule={onUpdateConditionalRule}
|
|
772
|
+
filterFields={filterFields}
|
|
773
|
+
currentView={view}
|
|
774
|
+
onViewChange={onViewChange}
|
|
775
|
+
lifecycleTabLabel={lifecycleDrawerLabel}
|
|
776
|
+
fieldDefinitions={fieldDefinitionsForDrawer}
|
|
777
|
+
resolveColumnLabel={resolveColumnLabel}
|
|
778
|
+
displayOptions={displayOptions}
|
|
779
|
+
onDisplayOptionsChange={onDisplayOptionsChange}
|
|
780
|
+
renderFilterOptionValue={renderFilterOptionValue}
|
|
781
|
+
/>
|
|
782
|
+
)}
|
|
783
|
+
/>
|
|
784
|
+
<ListPageTreePanelShell
|
|
785
|
+
resizableGroupId={`data-list-tree-${lifecycleTabId}`}
|
|
786
|
+
ariaLabel="Record outline and details"
|
|
787
|
+
tree={
|
|
788
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
789
|
+
<ListPageTreeColumnHeader title="Records" />
|
|
790
|
+
{listRows.length === 0 ? (
|
|
791
|
+
<p className="p-3 text-sm text-muted-foreground">{emptyTableCopy}</p>
|
|
792
|
+
) : (
|
|
793
|
+
<ul
|
|
794
|
+
role="tree"
|
|
795
|
+
aria-label="Demo records"
|
|
796
|
+
className="min-h-0 flex-1 list-none space-y-0.5 overflow-y-auto py-1"
|
|
797
|
+
>
|
|
798
|
+
{listRows.map(row => {
|
|
799
|
+
const isSel = selectedId === row.id
|
|
800
|
+
return (
|
|
801
|
+
<li key={row.id} role="none" className="py-0.5">
|
|
802
|
+
<button
|
|
803
|
+
type="button"
|
|
804
|
+
role="treeitem"
|
|
805
|
+
aria-selected={isSel}
|
|
806
|
+
tabIndex={isSel ? 0 : -1}
|
|
807
|
+
onClick={() => setSelectedId(row.id)}
|
|
808
|
+
className={cn(
|
|
809
|
+
"flex w-full min-h-8 items-center rounded-md px-3 py-2 text-left text-sm transition-colors duration-75",
|
|
810
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
|
|
811
|
+
isSel
|
|
812
|
+
? "bg-accent font-medium text-accent-foreground"
|
|
813
|
+
: "text-foreground hover:bg-muted/50",
|
|
814
|
+
)}
|
|
815
|
+
>
|
|
816
|
+
<span className="min-w-0 truncate">{row.student}</span>
|
|
817
|
+
</button>
|
|
818
|
+
</li>
|
|
819
|
+
)
|
|
820
|
+
})}
|
|
821
|
+
</ul>
|
|
822
|
+
)}
|
|
823
|
+
</div>
|
|
824
|
+
}
|
|
825
|
+
details={
|
|
826
|
+
selected ? (
|
|
827
|
+
<div className="flex min-h-0 flex-1 flex-col overflow-hidden bg-card">
|
|
828
|
+
<ListPageTreeColumnHeader title="Details" />
|
|
829
|
+
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
830
|
+
<PlacementFinderDetail
|
|
831
|
+
row={selected}
|
|
832
|
+
tab={lifecycleTabId as BoardCardLifecycleTabId}
|
|
833
|
+
hiddenColKeys={state.hiddenCols}
|
|
834
|
+
boardColumns={boardColumns}
|
|
835
|
+
/>
|
|
836
|
+
</div>
|
|
837
|
+
</div>
|
|
838
|
+
) : (
|
|
839
|
+
<ListPageSplitDetailsPlaceholder title="Nothing selected" />
|
|
840
|
+
)
|
|
841
|
+
}
|
|
842
|
+
/>
|
|
843
|
+
</>
|
|
844
|
+
)
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// ─── Placement-specific list row for FinderPanelView ─────────────────────────
|
|
848
|
+
|
|
849
|
+
function PlacementFinderListRow({
|
|
850
|
+
row,
|
|
851
|
+
isSelected,
|
|
852
|
+
tab,
|
|
853
|
+
hiddenColKeys,
|
|
854
|
+
boardColumns,
|
|
855
|
+
conditionalRules,
|
|
856
|
+
}: {
|
|
857
|
+
row: Placement
|
|
858
|
+
isSelected: boolean
|
|
859
|
+
tab: BoardCardLifecycleTabId
|
|
860
|
+
hiddenColKeys: Set<string>
|
|
861
|
+
boardColumns: ColumnDef<Placement>[]
|
|
862
|
+
conditionalRules?: ConditionalRule[]
|
|
863
|
+
}) {
|
|
864
|
+
const ruleBg = getConditionalRowBackground(row, conditionalRules)
|
|
865
|
+
const showStudent = isBoardFieldActive("student", tab, hiddenColKeys, boardColumns)
|
|
866
|
+
const showSite = isBoardFieldActive("site", tab, hiddenColKeys, boardColumns)
|
|
867
|
+
const name = showStudent ? row.student : `Placement ${row.id}`
|
|
868
|
+
|
|
869
|
+
return (
|
|
870
|
+
<div
|
|
871
|
+
className={`flex w-full min-w-0 items-center gap-3 transition-colors duration-75 ${
|
|
872
|
+
isSelected
|
|
873
|
+
? "bg-transparent text-accent-foreground"
|
|
874
|
+
: cn("text-foreground", ruleBg)
|
|
875
|
+
}`}
|
|
876
|
+
>
|
|
877
|
+
<AvatarInitials
|
|
878
|
+
initials={row.initials}
|
|
879
|
+
className={cn(
|
|
880
|
+
"size-8 shrink-0 rounded-full text-[11px] font-semibold",
|
|
881
|
+
isSelected ? "ring-2 ring-accent-foreground/35" : "",
|
|
882
|
+
)}
|
|
883
|
+
/>
|
|
884
|
+
<div className="min-w-0 flex-1">
|
|
885
|
+
<p className={cn("truncate text-[13px] font-medium leading-tight", isSelected ? "text-accent-foreground" : "text-foreground")}>
|
|
886
|
+
{name}
|
|
887
|
+
</p>
|
|
888
|
+
{showSite && (
|
|
889
|
+
<p className={cn("mt-0.5 truncate text-[11px] leading-tight", isSelected ? "text-accent-foreground/80" : "text-muted-foreground")}>
|
|
890
|
+
{row.site}
|
|
891
|
+
</p>
|
|
892
|
+
)}
|
|
893
|
+
</div>
|
|
894
|
+
{!isSelected && <StatusBadge status={row.status} />}
|
|
895
|
+
</div>
|
|
896
|
+
)
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// ─── Placement-specific detail pane for FinderPanelView ──────────────────────
|
|
900
|
+
|
|
901
|
+
function PlacementFinderDetail({
|
|
902
|
+
row,
|
|
903
|
+
tab,
|
|
904
|
+
hiddenColKeys,
|
|
905
|
+
boardColumns,
|
|
906
|
+
}: {
|
|
907
|
+
row: Placement
|
|
908
|
+
tab: BoardCardLifecycleTabId
|
|
909
|
+
hiddenColKeys: Set<string>
|
|
910
|
+
boardColumns: ColumnDef<Placement>[]
|
|
911
|
+
}) {
|
|
912
|
+
const router = useRouter()
|
|
913
|
+
const show = (key: string) => isBoardFieldActive(key, tab, hiddenColKeys, boardColumns)
|
|
914
|
+
|
|
915
|
+
return (
|
|
916
|
+
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
917
|
+
{/* Header */}
|
|
918
|
+
<div className="flex shrink-0 items-start gap-4 border-b border-border px-5 py-4">
|
|
919
|
+
<AvatarInitials initials={row.initials} className="size-14 shrink-0 rounded-full text-lg font-semibold" />
|
|
920
|
+
<div className="min-w-0 flex-1">
|
|
921
|
+
<h2 className="text-base font-semibold text-foreground leading-tight">{row.student}</h2>
|
|
922
|
+
{show("program") && <p className="mt-0.5 text-[13px] text-muted-foreground">{row.program}</p>}
|
|
923
|
+
{show("status") && <div className="mt-2"><StatusBadge status={row.status} /></div>}
|
|
924
|
+
</div>
|
|
925
|
+
<Tip side="bottom" label="Open full detail page">
|
|
926
|
+
<Button type="button" variant="outline" size="sm" className="shrink-0"
|
|
927
|
+
onClick={() => router.push(`/data-list/${row.id}`)}
|
|
928
|
+
aria-label={`Open full detail for ${row.student}`}>
|
|
929
|
+
<i className="fa-light fa-arrow-up-right-from-square text-[12px]" aria-hidden="true" />
|
|
930
|
+
Open
|
|
931
|
+
</Button>
|
|
932
|
+
</Tip>
|
|
933
|
+
</div>
|
|
934
|
+
|
|
935
|
+
{/* Fields */}
|
|
936
|
+
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
|
|
937
|
+
<dl className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
938
|
+
{show("email") && (
|
|
939
|
+
<div className="flex flex-col gap-0.5">
|
|
940
|
+
<dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
|
|
941
|
+
<i className="fa-light fa-envelope text-[10px]" aria-hidden="true" /> Email
|
|
942
|
+
</dt>
|
|
943
|
+
<dd className="text-[13px]">
|
|
944
|
+
<a href={`mailto:${row.email}`} className="text-interactive-foreground hover:underline">{row.email}</a>
|
|
945
|
+
</dd>
|
|
946
|
+
</div>
|
|
947
|
+
)}
|
|
948
|
+
{show("site") && (
|
|
949
|
+
<div className="flex flex-col gap-0.5">
|
|
950
|
+
<dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
|
|
951
|
+
<i className="fa-light fa-building text-[10px]" aria-hidden="true" /> Site
|
|
952
|
+
</dt>
|
|
953
|
+
<dd className="text-[13px] text-foreground">{row.site}</dd>
|
|
954
|
+
</div>
|
|
955
|
+
)}
|
|
956
|
+
{show("internship") && (
|
|
957
|
+
<div className="flex flex-col gap-0.5">
|
|
958
|
+
<dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
|
|
959
|
+
<i className="fa-light fa-briefcase text-[10px]" aria-hidden="true" /> Internship
|
|
960
|
+
</dt>
|
|
961
|
+
<dd className="text-[13px] text-foreground">{row.internship}</dd>
|
|
962
|
+
</div>
|
|
963
|
+
)}
|
|
964
|
+
{show("specialization") && (
|
|
965
|
+
<div className="flex flex-col gap-0.5">
|
|
966
|
+
<dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
|
|
967
|
+
<i className="fa-light fa-stethoscope text-[10px]" aria-hidden="true" /> Specialization
|
|
968
|
+
</dt>
|
|
969
|
+
<dd className="text-[13px] text-foreground">{row.specialization}</dd>
|
|
970
|
+
</div>
|
|
971
|
+
)}
|
|
972
|
+
{show("supervisor") && (
|
|
973
|
+
<div className="flex flex-col gap-0.5">
|
|
974
|
+
<dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
|
|
975
|
+
<i className="fa-light fa-user-tie text-[10px]" aria-hidden="true" /> Supervisor
|
|
976
|
+
</dt>
|
|
977
|
+
<dd className="text-[13px] text-foreground">{row.supervisor}</dd>
|
|
978
|
+
</div>
|
|
979
|
+
)}
|
|
980
|
+
{show("start") && (
|
|
981
|
+
<div className="flex flex-col gap-0.5">
|
|
982
|
+
<dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
|
|
983
|
+
<i className="fa-light fa-calendar text-[10px]" aria-hidden="true" /> Start Date
|
|
984
|
+
</dt>
|
|
985
|
+
<dd className="text-[13px] text-foreground">{row.start}</dd>
|
|
986
|
+
</div>
|
|
987
|
+
)}
|
|
988
|
+
{show("duration") && (
|
|
989
|
+
<div className="flex flex-col gap-0.5">
|
|
990
|
+
<dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
|
|
991
|
+
<i className="fa-light fa-clock text-[10px]" aria-hidden="true" /> Duration
|
|
992
|
+
</dt>
|
|
993
|
+
<dd className="text-[13px] text-foreground">{row.duration}</dd>
|
|
994
|
+
</div>
|
|
995
|
+
)}
|
|
996
|
+
{tab === "ongoing" && (
|
|
997
|
+
<div className="flex flex-col gap-0.5 sm:col-span-2">
|
|
998
|
+
<dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
|
|
999
|
+
<i className="fa-light fa-chart-line text-[10px]" aria-hidden="true" /> Progress
|
|
1000
|
+
</dt>
|
|
1001
|
+
<dd className="text-[13px] text-foreground flex flex-col gap-1.5">
|
|
1002
|
+
<span>{row.progressWeeksDone} / {row.progressWeeksTotal} weeks</span>
|
|
1003
|
+
<div role="progressbar" aria-valuenow={row.progressWeeksDone} aria-valuemin={0} aria-valuemax={row.progressWeeksTotal}
|
|
1004
|
+
aria-label={`${row.progressWeeksDone} of ${row.progressWeeksTotal} weeks completed`}
|
|
1005
|
+
className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
|
|
1006
|
+
<div className="h-full rounded-full bg-primary transition-all"
|
|
1007
|
+
style={{ width: `${Math.round((row.progressWeeksDone / Math.max(1, row.progressWeeksTotal)) * 100)}%` }} />
|
|
1008
|
+
</div>
|
|
1009
|
+
</dd>
|
|
1010
|
+
</div>
|
|
1011
|
+
)}
|
|
1012
|
+
{row.siteAddress && (
|
|
1013
|
+
<div className="flex flex-col gap-0.5 sm:col-span-2">
|
|
1014
|
+
<dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
|
|
1015
|
+
<i className="fa-light fa-location-dot text-[10px]" aria-hidden="true" /> Site Address
|
|
1016
|
+
</dt>
|
|
1017
|
+
<dd className="text-[13px] text-foreground">{row.siteAddress}</dd>
|
|
1018
|
+
</div>
|
|
1019
|
+
)}
|
|
1020
|
+
</dl>
|
|
1021
|
+
</div>
|
|
1022
|
+
</div>
|
|
1023
|
+
)
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// ─── Status groups for FinderPanelView ───────────────────────────────────────
|
|
1027
|
+
|
|
1028
|
+
const STATUS_GROUPS: Array<{ id: Status | "all"; label: string; accent: string }> = [
|
|
1029
|
+
{ id: "all", label: "All", accent: "bg-muted-foreground" },
|
|
1030
|
+
{ id: "confirmed", label: "Confirmed", accent: "bg-success" },
|
|
1031
|
+
{ id: "pending", label: "Pending", accent: "bg-warning" },
|
|
1032
|
+
{ id: "under-review", label: "Under Review", accent: "bg-brand" },
|
|
1033
|
+
{ id: "rejected", label: "Rejected", accent: "bg-destructive" },
|
|
1034
|
+
{ id: "completed", label: "Completed", accent: "bg-muted-foreground/50" },
|
|
1035
|
+
]
|
|
1036
|
+
|
|
1037
|
+
function buildStatusGroups(rows: Placement[]): FinderGroup[] {
|
|
1038
|
+
return STATUS_GROUPS.map(sg => ({
|
|
1039
|
+
id: sg.id,
|
|
1040
|
+
label: sg.label,
|
|
1041
|
+
accent: sg.accent,
|
|
1042
|
+
count: sg.id === "all" ? rows.length : rows.filter(r => r.status === sg.id).length,
|
|
1043
|
+
}))
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// ─── Panel view shell ────────────────────────────────────────────────────────
|
|
1047
|
+
|
|
1048
|
+
/** Finder-style panel view shell with groups, list, and detail pane */
|
|
1049
|
+
function DataListPanelShell({
|
|
1050
|
+
state,
|
|
1051
|
+
openDrawerRef,
|
|
1052
|
+
tableData,
|
|
1053
|
+
columns,
|
|
1054
|
+
lifecycleTabId,
|
|
1055
|
+
view,
|
|
1056
|
+
onViewChange,
|
|
1057
|
+
pagination,
|
|
1058
|
+
onPaginationChange,
|
|
1059
|
+
conditionalRules,
|
|
1060
|
+
onAddConditionalRule,
|
|
1061
|
+
onRemoveConditionalRule,
|
|
1062
|
+
onUpdateConditionalRule,
|
|
1063
|
+
filterFields,
|
|
1064
|
+
lifecycleDrawerLabel,
|
|
1065
|
+
fieldDefinitionsForDrawer,
|
|
1066
|
+
resolveColumnLabel,
|
|
1067
|
+
renderFilterOptionValue,
|
|
1068
|
+
displayOptions,
|
|
1069
|
+
onDisplayOptionsChange,
|
|
1070
|
+
listRows,
|
|
1071
|
+
emptyTableCopy,
|
|
1072
|
+
panelGroupsBuilder,
|
|
1073
|
+
panelRenderListRow,
|
|
1074
|
+
panelRenderDetail,
|
|
1075
|
+
}: {
|
|
1076
|
+
state: ReturnType<typeof useTableState<Placement>>
|
|
1077
|
+
openDrawerRef: React.MutableRefObject<() => void>
|
|
1078
|
+
tableData: Placement[]
|
|
1079
|
+
columns: ColumnDef<Placement>[]
|
|
1080
|
+
lifecycleTabId: PlacementLifecycleTabId
|
|
1081
|
+
view: DataListViewType
|
|
1082
|
+
onViewChange?: (view: DataListViewType) => void
|
|
1083
|
+
pagination: boolean
|
|
1084
|
+
onPaginationChange: (v: boolean) => void
|
|
1085
|
+
conditionalRules: ConditionalRule[]
|
|
1086
|
+
onAddConditionalRule: (rule: Omit<ConditionalRule, "id">) => void
|
|
1087
|
+
onRemoveConditionalRule: (id: string) => void
|
|
1088
|
+
onUpdateConditionalRule: (id: string, patch: Partial<ConditionalRule>) => void
|
|
1089
|
+
filterFields: FilterFieldDef[]
|
|
1090
|
+
lifecycleDrawerLabel: string
|
|
1091
|
+
fieldDefinitionsForDrawer: { key: string; label: string; sortable?: boolean }[]
|
|
1092
|
+
resolveColumnLabel: (key: string) => string
|
|
1093
|
+
renderFilterOptionValue: (fieldKey: string, value: string) => React.ReactNode
|
|
1094
|
+
displayOptions: DataListDisplayOptions
|
|
1095
|
+
onDisplayOptionsChange: (patch: Partial<DataListDisplayOptions>) => void
|
|
1096
|
+
listRows: Placement[]
|
|
1097
|
+
emptyTableCopy: string
|
|
1098
|
+
panelGroupsBuilder?: (rows: Placement[]) => FinderGroup[]
|
|
1099
|
+
panelRenderListRow?: (row: Placement, isSelected: boolean) => React.ReactNode
|
|
1100
|
+
panelRenderDetail?: (row: Placement) => React.ReactNode
|
|
1101
|
+
}) {
|
|
1102
|
+
React.useEffect(() => {
|
|
1103
|
+
openDrawerRef.current = () => state.setSheetOpen(true)
|
|
1104
|
+
}, [state.setSheetOpen])
|
|
1105
|
+
|
|
1106
|
+
const boardColumns = state.displayCols.filter(c => c.key !== "select" && c.key !== "actions")
|
|
1107
|
+
const groups = React.useMemo(
|
|
1108
|
+
() => panelGroupsBuilder ? panelGroupsBuilder(listRows) : buildStatusGroups(listRows),
|
|
1109
|
+
[listRows, panelGroupsBuilder],
|
|
1110
|
+
)
|
|
1111
|
+
|
|
1112
|
+
return (
|
|
1113
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
1114
|
+
<DataTableToolbar
|
|
1115
|
+
state={state}
|
|
1116
|
+
columns={columns}
|
|
1117
|
+
searchable
|
|
1118
|
+
renderFilterOptionValue={renderFilterOptionValue}
|
|
1119
|
+
searchAriaLabel="Search rows"
|
|
1120
|
+
toolbarSlot={s => (
|
|
1121
|
+
<TablePropertiesDrawerButton
|
|
1122
|
+
state={s}
|
|
1123
|
+
totalRows={tableData.length}
|
|
1124
|
+
pagination={pagination}
|
|
1125
|
+
onPaginationChange={onPaginationChange}
|
|
1126
|
+
conditionalRules={conditionalRules}
|
|
1127
|
+
onAddConditionalRule={onAddConditionalRule}
|
|
1128
|
+
onRemoveConditionalRule={onRemoveConditionalRule}
|
|
1129
|
+
onUpdateConditionalRule={onUpdateConditionalRule}
|
|
1130
|
+
filterFields={filterFields}
|
|
1131
|
+
currentView={view}
|
|
1132
|
+
onViewChange={onViewChange}
|
|
1133
|
+
lifecycleTabLabel={lifecycleDrawerLabel}
|
|
1134
|
+
fieldDefinitions={fieldDefinitionsForDrawer}
|
|
1135
|
+
resolveColumnLabel={resolveColumnLabel}
|
|
1136
|
+
displayOptions={displayOptions}
|
|
1137
|
+
onDisplayOptionsChange={onDisplayOptionsChange}
|
|
1138
|
+
renderFilterOptionValue={renderFilterOptionValue}
|
|
1139
|
+
/>
|
|
1140
|
+
)}
|
|
1141
|
+
/>
|
|
1142
|
+
<ListPageSplitHubChrome aria-label={lifecycleDrawerLabel}>
|
|
1143
|
+
<FinderPanelView<Placement>
|
|
1144
|
+
embedded
|
|
1145
|
+
groupsColumnTitle="Status"
|
|
1146
|
+
groups={groups}
|
|
1147
|
+
rows={listRows}
|
|
1148
|
+
getRowId={r => r.id}
|
|
1149
|
+
getRowGroupId={r => r.status}
|
|
1150
|
+
defaultGroupId="all"
|
|
1151
|
+
autoSaveId="finder-panel-view"
|
|
1152
|
+
ariaLabel="Demo panel view"
|
|
1153
|
+
emptyList={<p>{emptyTableCopy}</p>}
|
|
1154
|
+
renderListRow={
|
|
1155
|
+
panelRenderListRow
|
|
1156
|
+
? panelRenderListRow
|
|
1157
|
+
: (row, isSelected) => (
|
|
1158
|
+
<PlacementFinderListRow
|
|
1159
|
+
row={row}
|
|
1160
|
+
isSelected={isSelected}
|
|
1161
|
+
tab={lifecycleTabId as BoardCardLifecycleTabId}
|
|
1162
|
+
hiddenColKeys={state.hiddenCols}
|
|
1163
|
+
boardColumns={boardColumns}
|
|
1164
|
+
conditionalRules={conditionalRules}
|
|
1165
|
+
/>
|
|
1166
|
+
)
|
|
1167
|
+
}
|
|
1168
|
+
renderDetail={
|
|
1169
|
+
panelRenderDetail
|
|
1170
|
+
? panelRenderDetail
|
|
1171
|
+
: row => (
|
|
1172
|
+
<PlacementFinderDetail
|
|
1173
|
+
row={row}
|
|
1174
|
+
tab={lifecycleTabId as BoardCardLifecycleTabId}
|
|
1175
|
+
hiddenColKeys={state.hiddenCols}
|
|
1176
|
+
boardColumns={boardColumns}
|
|
1177
|
+
/>
|
|
1178
|
+
)
|
|
1179
|
+
}
|
|
1180
|
+
/>
|
|
1181
|
+
</ListPageSplitHubChrome>
|
|
1182
|
+
</div>
|
|
1183
|
+
)
|
|
1184
|
+
}
|
|
1185
|
+
|
|
501
1186
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
502
1187
|
// Props
|
|
503
1188
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -505,7 +1190,7 @@ function DataListDashboardShell({
|
|
|
505
1190
|
export interface DataListTableProps {
|
|
506
1191
|
view?: DataListViewType
|
|
507
1192
|
onViewChange?: (view: DataListViewType) => void
|
|
508
|
-
/**
|
|
1193
|
+
/** Demo row segment: drives filtered rows + column set (`all` | `upcoming` | `ongoing` | `completed`). */
|
|
509
1194
|
lifecycleTabId?: PlacementLifecycleTabId
|
|
510
1195
|
/** Shared display options (persist at page level — all view types). */
|
|
511
1196
|
displayOptions?: DataListDisplayOptions
|
|
@@ -516,6 +1201,12 @@ export interface DataListTableProps {
|
|
|
516
1201
|
emptyTableCopy: string
|
|
517
1202
|
/** Table Properties drawer lifecycle label — from the page. */
|
|
518
1203
|
lifecycleDrawerLabel: string
|
|
1204
|
+
/** Panel view: custom groups builder. If not provided, uses default placement status groups. */
|
|
1205
|
+
panelGroupsBuilder?: (rows: Placement[]) => FinderGroup[]
|
|
1206
|
+
/** Panel view: custom list row renderer. If not provided, uses default placement row rendering. */
|
|
1207
|
+
panelRenderListRow?: (row: Placement, isSelected: boolean) => React.ReactNode
|
|
1208
|
+
/** Panel view: custom detail pane renderer. If not provided, uses default placement detail rendering. */
|
|
1209
|
+
panelRenderDetail?: (row: Placement) => React.ReactNode
|
|
519
1210
|
}
|
|
520
1211
|
|
|
521
1212
|
/** Imperative handle — open Table Properties (table view only). */
|
|
@@ -534,6 +1225,9 @@ export const DataListTable = React.forwardRef<DataListTableHandle, DataListTable
|
|
|
534
1225
|
getColumnsForLifecycle,
|
|
535
1226
|
emptyTableCopy,
|
|
536
1227
|
lifecycleDrawerLabel,
|
|
1228
|
+
panelGroupsBuilder,
|
|
1229
|
+
panelRenderListRow,
|
|
1230
|
+
panelRenderDetail,
|
|
537
1231
|
}, ref) {
|
|
538
1232
|
const displayOptions = React.useMemo(
|
|
539
1233
|
() => ({ ...DEFAULT_DATA_LIST_DISPLAY_OPTIONS, ...displayOptionsProp }),
|
|
@@ -618,7 +1312,7 @@ export const DataListTable = React.forwardRef<DataListTableHandle, DataListTable
|
|
|
618
1312
|
const totalPages = Math.max(1, Math.ceil(filteredCount / Math.max(1, paginationPageSize)))
|
|
619
1313
|
const safePage = Math.min(paginationPage, totalPages)
|
|
620
1314
|
const paginationOverride =
|
|
621
|
-
pagination && view !== "board" && view !== "dashboard"
|
|
1315
|
+
pagination && view !== "board" && view !== "dashboard" && view !== "folder" && view !== "panel" && view !== "tree-panel"
|
|
622
1316
|
? { page: safePage, pageSize: paginationPageSize }
|
|
623
1317
|
: undefined
|
|
624
1318
|
|
|
@@ -842,6 +1536,99 @@ export const DataListTable = React.forwardRef<DataListTableHandle, DataListTable
|
|
|
842
1536
|
)
|
|
843
1537
|
}
|
|
844
1538
|
|
|
1539
|
+
if (view === "folder") {
|
|
1540
|
+
return (
|
|
1541
|
+
<DataListFolderShell
|
|
1542
|
+
key={lifecycleTabId}
|
|
1543
|
+
state={tableState}
|
|
1544
|
+
openDrawerRef={openDrawerRef}
|
|
1545
|
+
tableData={tableData}
|
|
1546
|
+
columns={columns}
|
|
1547
|
+
lifecycleTabId={lifecycleTabId}
|
|
1548
|
+
view={view}
|
|
1549
|
+
onViewChange={onViewChange}
|
|
1550
|
+
pagination={pagination}
|
|
1551
|
+
onPaginationChange={setPagination}
|
|
1552
|
+
conditionalRules={conditionalRules}
|
|
1553
|
+
onAddConditionalRule={addConditionalRule}
|
|
1554
|
+
onRemoveConditionalRule={removeConditionalRule}
|
|
1555
|
+
onUpdateConditionalRule={updateConditionalRule}
|
|
1556
|
+
filterFields={filterFields}
|
|
1557
|
+
lifecycleDrawerLabel={lifecycleDrawerLabel}
|
|
1558
|
+
fieldDefinitionsForDrawer={fieldDefinitionsForDrawer}
|
|
1559
|
+
resolveColumnLabel={resolveColumnLabel}
|
|
1560
|
+
renderFilterOptionValue={renderFilterOptionValue}
|
|
1561
|
+
displayOptions={displayOptions}
|
|
1562
|
+
onDisplayOptionsChange={patchDisplayOptions}
|
|
1563
|
+
listRows={tableState.rows}
|
|
1564
|
+
emptyTableCopy={emptyTableCopy}
|
|
1565
|
+
/>
|
|
1566
|
+
)
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
if (view === "tree-panel") {
|
|
1570
|
+
return (
|
|
1571
|
+
<DataListTreeShell
|
|
1572
|
+
key={lifecycleTabId}
|
|
1573
|
+
state={tableState}
|
|
1574
|
+
openDrawerRef={openDrawerRef}
|
|
1575
|
+
tableData={tableData}
|
|
1576
|
+
columns={columns}
|
|
1577
|
+
lifecycleTabId={lifecycleTabId}
|
|
1578
|
+
view={view}
|
|
1579
|
+
onViewChange={onViewChange}
|
|
1580
|
+
pagination={pagination}
|
|
1581
|
+
onPaginationChange={setPagination}
|
|
1582
|
+
conditionalRules={conditionalRules}
|
|
1583
|
+
onAddConditionalRule={addConditionalRule}
|
|
1584
|
+
onRemoveConditionalRule={removeConditionalRule}
|
|
1585
|
+
onUpdateConditionalRule={updateConditionalRule}
|
|
1586
|
+
filterFields={filterFields}
|
|
1587
|
+
lifecycleDrawerLabel={lifecycleDrawerLabel}
|
|
1588
|
+
fieldDefinitionsForDrawer={fieldDefinitionsForDrawer}
|
|
1589
|
+
resolveColumnLabel={resolveColumnLabel}
|
|
1590
|
+
renderFilterOptionValue={renderFilterOptionValue}
|
|
1591
|
+
displayOptions={displayOptions}
|
|
1592
|
+
onDisplayOptionsChange={patchDisplayOptions}
|
|
1593
|
+
listRows={tableState.rows}
|
|
1594
|
+
emptyTableCopy={emptyTableCopy}
|
|
1595
|
+
/>
|
|
1596
|
+
)
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
if (view === "panel") {
|
|
1600
|
+
return (
|
|
1601
|
+
<DataListPanelShell
|
|
1602
|
+
key={lifecycleTabId}
|
|
1603
|
+
state={tableState}
|
|
1604
|
+
openDrawerRef={openDrawerRef}
|
|
1605
|
+
tableData={tableData}
|
|
1606
|
+
columns={columns}
|
|
1607
|
+
lifecycleTabId={lifecycleTabId}
|
|
1608
|
+
view={view}
|
|
1609
|
+
onViewChange={onViewChange}
|
|
1610
|
+
pagination={pagination}
|
|
1611
|
+
onPaginationChange={setPagination}
|
|
1612
|
+
conditionalRules={conditionalRules}
|
|
1613
|
+
onAddConditionalRule={addConditionalRule}
|
|
1614
|
+
onRemoveConditionalRule={removeConditionalRule}
|
|
1615
|
+
onUpdateConditionalRule={updateConditionalRule}
|
|
1616
|
+
filterFields={filterFields}
|
|
1617
|
+
lifecycleDrawerLabel={lifecycleDrawerLabel}
|
|
1618
|
+
fieldDefinitionsForDrawer={fieldDefinitionsForDrawer}
|
|
1619
|
+
resolveColumnLabel={resolveColumnLabel}
|
|
1620
|
+
renderFilterOptionValue={renderFilterOptionValue}
|
|
1621
|
+
displayOptions={displayOptions}
|
|
1622
|
+
onDisplayOptionsChange={patchDisplayOptions}
|
|
1623
|
+
listRows={tableState.rows}
|
|
1624
|
+
emptyTableCopy={emptyTableCopy}
|
|
1625
|
+
panelGroupsBuilder={panelGroupsBuilder}
|
|
1626
|
+
panelRenderListRow={panelRenderListRow}
|
|
1627
|
+
panelRenderDetail={panelRenderDetail}
|
|
1628
|
+
/>
|
|
1629
|
+
)
|
|
1630
|
+
}
|
|
1631
|
+
|
|
845
1632
|
if (pagination) {
|
|
846
1633
|
return (
|
|
847
1634
|
<React.Fragment key={lifecycleTabId}>
|