@exxatdesignux/ui 0.0.5 → 0.0.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/bin/init.mjs +29 -0
- package/package.json +7 -2
- package/template/.nvmrc +1 -0
- package/template/.prettierignore +7 -0
- package/template/.prettierrc +11 -0
- package/template/AGENTS.md +485 -0
- package/template/Logo/Exxat_Prism.svg +39 -0
- package/template/Logo/Exxat_one.svg +36 -0
- package/template/README.md +58 -0
- package/template/app/(app)/compliance/page.tsx +10 -0
- package/template/app/(app)/dashboard/loading.tsx +18 -0
- package/template/app/(app)/dashboard/page.tsx +36 -0
- package/template/app/(app)/data-list/[id]/page.tsx +28 -0
- package/template/app/(app)/data-list/new/page.tsx +31 -0
- package/template/app/(app)/data-list/page.tsx +10 -0
- package/template/app/(app)/error.tsx +43 -0
- package/template/app/(app)/help/page.tsx +34 -0
- package/template/app/(app)/layout.tsx +54 -0
- package/template/app/(app)/loading.tsx +18 -0
- package/template/app/(app)/question-bank/page.tsx +10 -0
- package/template/app/(app)/rotations/page.tsx +15 -0
- package/template/app/(app)/settings/page.tsx +17 -0
- package/template/app/(app)/sites/all/page.tsx +13 -0
- package/template/app/(app)/team/page.tsx +10 -0
- package/template/app/favicon.ico +0 -0
- package/template/app/globals.css +1811 -0
- package/template/app/layout.tsx +95 -0
- package/template/app/page.tsx +9 -0
- package/template/components/.gitkeep +0 -0
- package/template/components/app-sidebar-dynamic.tsx +15 -0
- package/template/components/app-sidebar.tsx +901 -0
- package/template/components/ask-leo-composer.tsx +216 -0
- package/template/components/ask-leo-sidebar.tsx +509 -0
- package/template/components/chart-area-interactive.tsx +293 -0
- package/template/components/charts-overview.tsx +2321 -0
- package/template/components/command-menu-01.tsx +133 -0
- package/template/components/command-menu-02.tsx +386 -0
- package/template/components/command-menu.tsx +182 -0
- package/template/components/compliance-board-view.tsx +134 -0
- package/template/components/compliance-client.tsx +92 -0
- package/template/components/compliance-list-view.tsx +59 -0
- package/template/components/compliance-page-header.tsx +89 -0
- package/template/components/compliance-table.tsx +525 -0
- package/template/components/dashboard-onboarding-gallery.tsx +13 -0
- package/template/components/dashboard-onboarding.tsx +21 -0
- package/template/components/dashboard-promo-banner.tsx +67 -0
- package/template/components/dashboard-quota-progress-card.tsx +369 -0
- package/template/components/dashboard-report-charts.tsx +69 -0
- package/template/components/dashboard-section-heading.tsx +68 -0
- package/template/components/dashboard-tabs.tsx +598 -0
- package/template/components/data-list-client.tsx +239 -0
- package/template/components/data-list-table-cells.test.tsx +22 -0
- package/template/components/data-list-table-cells.tsx +173 -0
- package/template/components/data-list-table.tsx +879 -0
- package/template/components/data-table/filter-date-calendar.tsx +38 -0
- package/template/components/data-table/filter-text-value-input.tsx +77 -0
- package/template/components/data-table/index.tsx +1612 -0
- package/template/components/data-table/pagination.tsx +256 -0
- package/template/components/data-table/types.ts +91 -0
- package/template/components/data-table/use-table-state.ts +566 -0
- package/template/components/data-view-dashboard-charts-compliance.tsx +960 -0
- package/template/components/data-view-dashboard-charts-team.tsx +968 -0
- package/template/components/data-view-dashboard-charts.tsx +1668 -0
- package/template/components/data-views/board-card-primitives.tsx +93 -0
- package/template/components/data-views/index.ts +41 -0
- package/template/components/data-views/list-page-board-card.tsx +192 -0
- package/template/components/data-views/list-page-board-template.tsx +122 -0
- package/template/components/data-views/placement-board-card.tsx +262 -0
- package/template/components/export-drawer.tsx +375 -0
- package/template/components/exxat-product-logo.tsx +453 -0
- package/template/components/form-layout-01.tsx +131 -0
- package/template/components/getting-started.tsx +625 -0
- package/template/components/key-metrics.tsx +920 -0
- package/template/components/leo-insight-indicator.tsx +364 -0
- package/template/components/leo-typing-dots.tsx +121 -0
- package/template/components/list-hub-status-badge.tsx +51 -0
- package/template/components/list-page-dashboard-charts.tsx +18 -0
- package/template/components/nav-documents.tsx +89 -0
- package/template/components/nav-main.tsx +58 -0
- package/template/components/nav-secondary.tsx +64 -0
- package/template/components/nav-user.tsx +190 -0
- package/template/components/new-placement-back-btn.tsx +28 -0
- package/template/components/new-placement-form.tsx +1066 -0
- package/template/components/onboarding/index.ts +4 -0
- package/template/components/onboarding/onboarding-01.tsx +7 -0
- package/template/components/onboarding/onboarding-02.tsx +7 -0
- package/template/components/onboarding/onboarding-03.tsx +7 -0
- package/template/components/onboarding/onboarding-04.tsx +7 -0
- package/template/components/page-header.tsx +57 -0
- package/template/components/placement-detail.tsx +438 -0
- package/template/components/placements-board-view.tsx +404 -0
- package/template/components/placements-list-view.tsx +285 -0
- package/template/components/placements-page-header.tsx +160 -0
- package/template/components/placements-table-columns.tsx +639 -0
- package/template/components/product-switcher.tsx +116 -0
- package/template/components/question-bank-board-view.tsx +205 -0
- package/template/components/question-bank-client.tsx +77 -0
- package/template/components/question-bank-list-view.tsx +59 -0
- package/template/components/question-bank-page-header.tsx +89 -0
- package/template/components/question-bank-table.tsx +586 -0
- package/template/components/rotations-empty-state.tsx +47 -0
- package/template/components/rotations-panel-activator.tsx +8 -0
- package/template/components/secondary-nav.tsx +394 -0
- package/template/components/secondary-panel.tsx +239 -0
- package/template/components/section-cards.tsx +106 -0
- package/template/components/settings-appearance-card.tsx +424 -0
- package/template/components/settings-client.tsx +537 -0
- package/template/components/settings-form-row.tsx +42 -0
- package/template/components/sidebar-auto-collapse.tsx +23 -0
- package/template/components/sidebar-auto-open.tsx +18 -0
- package/template/components/sidebar-shell.tsx +37 -0
- package/template/components/site-header.tsx +93 -0
- package/template/components/sites-all-client.tsx +154 -0
- package/template/components/sites-board-view.tsx +67 -0
- package/template/components/sites-list-view.tsx +47 -0
- package/template/components/sites-table.tsx +312 -0
- package/template/components/system-banner-slot.tsx +66 -0
- package/template/components/table-properties/column-row.tsx +90 -0
- package/template/components/table-properties/draggable-list.ts +49 -0
- package/template/components/table-properties/drawer-button.tsx +231 -0
- package/template/components/table-properties/drawer.tsx +1102 -0
- package/template/components/table-properties/filter-card.tsx +251 -0
- package/template/components/table-properties/index.ts +22 -0
- package/template/components/table-properties/sort-card.tsx +59 -0
- package/template/components/table-properties/types.ts +124 -0
- package/template/components/task-list-panel.tsx +98 -0
- package/template/components/task-priority-badge.tsx +28 -0
- package/template/components/team-board-view.tsx +114 -0
- package/template/components/team-client.tsx +93 -0
- package/template/components/team-list-view.tsx +62 -0
- package/template/components/team-page-header.tsx +92 -0
- package/template/components/team-table.tsx +525 -0
- package/template/components/templates/list-page.tsx +576 -0
- package/template/components/templates/primary-page-template.tsx +56 -0
- package/template/components/theme-color-sync.tsx +32 -0
- package/template/components/theme-provider.tsx +71 -0
- package/template/components/tinted-icon-disc.tsx +53 -0
- package/template/components/ui/ai-thinking-surface.tsx +121 -0
- package/template/components/ui/avatar.tsx +1 -0
- package/template/components/ui/badge.tsx +1 -0
- package/template/components/ui/banner.tsx +1 -0
- package/template/components/ui/breadcrumb.tsx +1 -0
- package/template/components/ui/button.tsx +1 -0
- package/template/components/ui/calendar.tsx +1 -0
- package/template/components/ui/card.tsx +1 -0
- package/template/components/ui/chart.tsx +1 -0
- package/template/components/ui/checkbox.tsx +1 -0
- package/template/components/ui/coach-mark.tsx +1 -0
- package/template/components/ui/collapsible.tsx +1 -0
- package/template/components/ui/command.tsx +1 -0
- package/template/components/ui/date-picker-field.tsx +1 -0
- package/template/components/ui/dialog.tsx +1 -0
- package/template/components/ui/dot-pattern.tsx +159 -0
- package/template/components/ui/drag-handle-grip.tsx +1 -0
- package/template/components/ui/drawer.tsx +1 -0
- package/template/components/ui/dropdown-menu.tsx +1 -0
- package/template/components/ui/field.tsx +1 -0
- package/template/components/ui/form.tsx +1 -0
- package/template/components/ui/input-group.tsx +1 -0
- package/template/components/ui/input-mask.tsx +1 -0
- package/template/components/ui/input.tsx +1 -0
- package/template/components/ui/kbd.tsx +1 -0
- package/template/components/ui/label.tsx +1 -0
- package/template/components/ui/leo-icon.tsx +726 -0
- package/template/components/ui/payment-card-fields.tsx +1 -0
- package/template/components/ui/popover.tsx +1 -0
- package/template/components/ui/radio-group.tsx +1 -0
- package/template/components/ui/select.tsx +1 -0
- package/template/components/ui/selection-tile-grid.tsx +1 -0
- package/template/components/ui/separator.tsx +1 -0
- package/template/components/ui/sheet.tsx +1 -0
- package/template/components/ui/sidebar.tsx +1 -0
- package/template/components/ui/skeleton.tsx +1 -0
- package/template/components/ui/sonner.tsx +1 -0
- package/template/components/ui/status-badge.tsx +1 -0
- package/template/components/ui/table.tsx +1 -0
- package/template/components/ui/tabs.tsx +1 -0
- package/template/components/ui/textarea.tsx +1 -0
- package/template/components/ui/tip.tsx +1 -0
- package/template/components/ui/toggle-group.tsx +1 -0
- package/template/components/ui/toggle-switch.tsx +1 -0
- package/template/components/ui/toggle.tsx +1 -0
- package/template/components/ui/tooltip.tsx +1 -0
- package/template/components/ui/view-segmented-control.tsx +1 -0
- package/template/components.json +27 -0
- package/template/contexts/chart-variant-context.tsx +35 -0
- package/template/contexts/command-menu-context.tsx +28 -0
- package/template/contexts/dashboard-view-context.tsx +35 -0
- package/template/contexts/product-context.tsx +38 -0
- package/template/contexts/system-banner-context.tsx +127 -0
- package/template/docs/command-menu-pattern.md +45 -0
- package/template/docs/data-views-pattern.md +160 -0
- package/template/ecosystem.config.cjs +20 -0
- package/template/eslint.config.mjs +18 -0
- package/template/fontawesome-subset.manifest.json +190 -0
- package/template/hooks/.gitkeep +0 -0
- package/template/hooks/use-app-theme.ts +1 -0
- package/template/hooks/use-coach-mark.ts +1 -0
- package/template/hooks/use-mobile.ts +1 -0
- package/template/hooks/use-mod-key-label.ts +1 -0
- package/template/lib/.gitkeep +0 -0
- package/template/lib/ask-leo-route-context.ts +133 -0
- package/template/lib/chart-keyboard-selection.test.ts +20 -0
- package/template/lib/chart-keyboard-selection.ts +17 -0
- package/template/lib/chart-line-dash.ts +16 -0
- package/template/lib/coach-mark-registry.ts +68 -0
- package/template/lib/command-menu-config.ts +127 -0
- package/template/lib/command-menu-search-data.ts +44 -0
- package/template/lib/conditional-rule-match.ts +32 -0
- package/template/lib/dashboard-customize-coach-mark.ts +18 -0
- package/template/lib/dashboard-layout-merge.ts +63 -0
- package/template/lib/data-list-display-options.ts +35 -0
- package/template/lib/data-list-persistence.ts +280 -0
- package/template/lib/data-list-view-surface.ts +58 -0
- package/template/lib/data-list-view.ts +29 -0
- package/template/lib/data-view-dashboard-storage.ts +101 -0
- package/template/lib/date-filter.ts +8 -0
- package/template/lib/dev-log.test.ts +28 -0
- package/template/lib/dev-log.ts +8 -0
- package/template/lib/editable-target.ts +10 -0
- package/template/lib/floating-sheet-panel.ts +72 -0
- package/template/lib/initials-from-name.ts +7 -0
- package/template/lib/list-page-table-properties.ts +52 -0
- package/template/lib/list-status-badges.ts +168 -0
- package/template/lib/logo-dev.ts +12 -0
- package/template/lib/mock/compliance-kpi.ts +61 -0
- package/template/lib/mock/compliance.ts +146 -0
- package/template/lib/mock/dashboard.ts +105 -0
- package/template/lib/mock/navigation.tsx +231 -0
- package/template/lib/mock/placements-kpi.ts +134 -0
- package/template/lib/mock/placements.ts +183 -0
- package/template/lib/mock/question-bank-kpi.ts +61 -0
- package/template/lib/mock/question-bank.ts +142 -0
- package/template/lib/mock/sites-directory.ts +16 -0
- package/template/lib/mock/sites-kpi.ts +25 -0
- package/template/lib/mock/team-kpi.ts +60 -0
- package/template/lib/mock/team.ts +118 -0
- package/template/lib/motion-ui.ts +17 -0
- package/template/lib/placement-board-card-layout.ts +79 -0
- package/template/lib/placement-lifecycle.ts +5 -0
- package/template/lib/row-height.ts +10 -0
- package/template/lib/stock-portrait.ts +11 -0
- package/template/lib/utils.test.ts +13 -0
- package/template/lib/utils.ts +1 -0
- package/template/next.config.mjs +15 -0
- package/template/package.json +83 -0
- package/template/postcss.config.mjs +8 -0
- package/template/public/.gitkeep +0 -0
- package/template/public/Illustration/Rotation.svg +74 -0
- package/template/public/avatars/user.svg +11 -0
- package/template/public/favicon/favicon.ico +0 -0
- package/template/public/favicon.ico +0 -0
- package/template/public/logos/exxat-one.svg +36 -0
- package/template/public/logos/exxat-prism.svg +39 -0
- package/template/public/mock-schools/emory.svg +4 -0
- package/template/public/mock-schools/rush.svg +4 -0
- package/template/scripts/fontawesome-subset-audit.mjs +190 -0
- package/template/scripts/pm2-startup-macos.sh +13 -0
- package/template/skills-lock.json +10 -0
- package/template/stores/app-store.ts +33 -0
- package/template/tests/setup.ts +1 -0
- package/template/tsconfig.json +35 -0
- package/template/types/react-payment-inputs.d.ts +19 -0
- package/template/vitest.config.ts +18 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PlacementsBoardView — kanban-style board by lifecycle phase (domain-specific columns).
|
|
5
|
+
* View chrome labels use `dataListViewLabel` from `@/lib/data-list-view` at the page level;
|
|
6
|
+
* this component focuses on placement phase grouping + shared card primitives.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as React from "react"
|
|
10
|
+
import { useRouter } from "next/navigation"
|
|
11
|
+
import { cn } from "@/lib/utils"
|
|
12
|
+
import type { Placement, PlacementPhase } from "@/lib/mock/placements"
|
|
13
|
+
import { Input } from "@/components/ui/input"
|
|
14
|
+
import { Tip } from "@/components/ui/tip"
|
|
15
|
+
import {
|
|
16
|
+
DropdownMenu,
|
|
17
|
+
DropdownMenuContent,
|
|
18
|
+
DropdownMenuItem,
|
|
19
|
+
DropdownMenuSeparator,
|
|
20
|
+
DropdownMenuSub,
|
|
21
|
+
DropdownMenuSubContent,
|
|
22
|
+
DropdownMenuSubTrigger,
|
|
23
|
+
DropdownMenuTrigger,
|
|
24
|
+
} from "@/components/ui/dropdown-menu"
|
|
25
|
+
import { DEFAULT_DATA_LIST_DISPLAY_OPTIONS, type BoardLineCount } from "@/lib/data-list-display-options"
|
|
26
|
+
import { type BoardCardLifecycleTabId } from "@/lib/placement-board-card-layout"
|
|
27
|
+
import type { ConditionalRule } from "@/components/table-properties/types"
|
|
28
|
+
import type { ColumnDef } from "@/components/data-table/types"
|
|
29
|
+
import { Badge } from "@/components/ui/badge"
|
|
30
|
+
import { BoardPlacementCard } from "@/components/data-views/placement-board-card"
|
|
31
|
+
import { BoardNewCardPlaceholder } from "@/components/data-views/board-card-primitives"
|
|
32
|
+
|
|
33
|
+
const PHASE_COLUMNS: { phase: PlacementPhase; label: string; description: string }[] = [
|
|
34
|
+
{ phase: "upcoming", label: "Upcoming", description: "Starting soon" },
|
|
35
|
+
{ phase: "ongoing", label: "Ongoing", description: "In progress" },
|
|
36
|
+
{ phase: "completed", label: "Completed", description: "Finished" },
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
/** Substring match across visible card fields (per-phase quick search). */
|
|
40
|
+
function rowMatchesPhaseSearch(row: Placement, q: string): boolean {
|
|
41
|
+
if (!q.trim()) return true
|
|
42
|
+
const lower = q.toLowerCase()
|
|
43
|
+
const hay = [
|
|
44
|
+
row.student,
|
|
45
|
+
row.site,
|
|
46
|
+
row.specialization,
|
|
47
|
+
row.internship,
|
|
48
|
+
row.program,
|
|
49
|
+
row.status,
|
|
50
|
+
row.supervisor,
|
|
51
|
+
row.email,
|
|
52
|
+
row.start,
|
|
53
|
+
]
|
|
54
|
+
.map(v => String(v ?? "").toLowerCase())
|
|
55
|
+
.join(" ")
|
|
56
|
+
return hay.includes(lower)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface PlacementsBoardColumnMenu {
|
|
60
|
+
filterableColumns: { key: string; label: string }[]
|
|
61
|
+
sortableColumns: { key: string; label: string }[]
|
|
62
|
+
groupableColumns: { key: string; label: string }[]
|
|
63
|
+
groupBy: string | null
|
|
64
|
+
onAddFilter: (fieldKey: string) => void
|
|
65
|
+
onSortByField: (fieldKey: string, direction: "asc" | "desc") => void
|
|
66
|
+
onToggleGroupBy: (fieldKey: string) => void
|
|
67
|
+
onOpenProperties: () => void
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface BoardDisplaySettings {
|
|
71
|
+
lineCount: BoardLineCount
|
|
72
|
+
showColumnLabels: boolean
|
|
73
|
+
showColumnCounts: boolean
|
|
74
|
+
newCardAbove: boolean
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface PlacementsBoardViewProps {
|
|
78
|
+
placements: Placement[]
|
|
79
|
+
/** Current lifecycle filter tab — drives helper copy above the board. */
|
|
80
|
+
lifecycleTabId: BoardCardLifecycleTabId
|
|
81
|
+
/** When set, each phase column header shows the same actions as a DataTable column header. */
|
|
82
|
+
boardColumnMenu?: PlacementsBoardColumnMenu
|
|
83
|
+
/** Board display options (Properties → view display). */
|
|
84
|
+
boardDisplay?: BoardDisplaySettings
|
|
85
|
+
/** Column visibility from table state — hidden columns omit matching card fields. */
|
|
86
|
+
hiddenColKeys?: Set<string>
|
|
87
|
+
/** Same conditional formatting as the table (row background when a rule matches). */
|
|
88
|
+
conditionalRules?: ConditionalRule[]
|
|
89
|
+
/** Visible data columns (table order) — drives dates and other fields on the card. */
|
|
90
|
+
boardColumns: ColumnDef<Placement>[]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function BoardPhaseColumnHeader({
|
|
94
|
+
label,
|
|
95
|
+
rawCount,
|
|
96
|
+
filteredCount,
|
|
97
|
+
searchValue,
|
|
98
|
+
onSearchChange,
|
|
99
|
+
menu,
|
|
100
|
+
showLabels,
|
|
101
|
+
showCounts,
|
|
102
|
+
}: {
|
|
103
|
+
label: string
|
|
104
|
+
rawCount: number
|
|
105
|
+
filteredCount: number
|
|
106
|
+
searchValue: string
|
|
107
|
+
onSearchChange: (value: string) => void
|
|
108
|
+
menu: PlacementsBoardColumnMenu
|
|
109
|
+
showLabels: boolean
|
|
110
|
+
showCounts: boolean
|
|
111
|
+
}) {
|
|
112
|
+
const searchActive = Boolean(searchValue.trim())
|
|
113
|
+
const countLabel =
|
|
114
|
+
searchActive && filteredCount !== rawCount
|
|
115
|
+
? `${filteredCount} of ${rawCount} records`
|
|
116
|
+
: `${filteredCount} ${filteredCount === 1 ? "record" : "records"}`
|
|
117
|
+
|
|
118
|
+
const showLeft = showLabels || showCounts
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div className="group/board-col border-b border-border px-3 py-2.5">
|
|
122
|
+
<div className="flex items-center justify-between gap-2">
|
|
123
|
+
{showLeft ? (
|
|
124
|
+
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
125
|
+
{showLabels ? (
|
|
126
|
+
<p className="min-w-0 truncate text-sm font-semibold text-foreground">{label}</p>
|
|
127
|
+
) : null}
|
|
128
|
+
{showCounts ? (
|
|
129
|
+
<div className="flex shrink-0 items-center gap-1.5">
|
|
130
|
+
<Badge
|
|
131
|
+
variant="outline"
|
|
132
|
+
className="inline-flex h-6 min-w-6 items-center justify-center border-0 bg-muted/70 px-2 text-xs font-semibold tabular-nums text-foreground"
|
|
133
|
+
aria-label={countLabel}
|
|
134
|
+
>
|
|
135
|
+
{filteredCount}
|
|
136
|
+
</Badge>
|
|
137
|
+
{searchActive && filteredCount !== rawCount ? (
|
|
138
|
+
<span className="text-xs font-medium tabular-nums text-muted-foreground" aria-hidden>
|
|
139
|
+
/ {rawCount}
|
|
140
|
+
</span>
|
|
141
|
+
) : null}
|
|
142
|
+
</div>
|
|
143
|
+
) : null}
|
|
144
|
+
</div>
|
|
145
|
+
) : (
|
|
146
|
+
<div className="min-w-0 flex-1" aria-hidden />
|
|
147
|
+
)}
|
|
148
|
+
<DropdownMenu>
|
|
149
|
+
<Tip label="Column options" side="top">
|
|
150
|
+
<DropdownMenuTrigger asChild>
|
|
151
|
+
<button
|
|
152
|
+
type="button"
|
|
153
|
+
aria-label={`${label} column options`}
|
|
154
|
+
onClick={e => e.stopPropagation()}
|
|
155
|
+
className={cn(
|
|
156
|
+
"opacity-0 group-hover/board-col:opacity-100 group-focus-within/board-col:opacity-100",
|
|
157
|
+
"inline-flex shrink-0 items-center justify-center size-7 rounded-md",
|
|
158
|
+
"text-muted-foreground hover:text-interactive-hover-foreground hover:bg-interactive-hover-row",
|
|
159
|
+
"transition-opacity focus-visible:opacity-100",
|
|
160
|
+
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
161
|
+
)}
|
|
162
|
+
>
|
|
163
|
+
<i className="fa-light fa-ellipsis-vertical text-xs" aria-hidden="true" />
|
|
164
|
+
</button>
|
|
165
|
+
</DropdownMenuTrigger>
|
|
166
|
+
</Tip>
|
|
167
|
+
<DropdownMenuContent align="end" className="min-w-44">
|
|
168
|
+
<div className="px-2 pt-2 pb-1">
|
|
169
|
+
<div className="relative">
|
|
170
|
+
<i
|
|
171
|
+
className="fa-light fa-magnifying-glass pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 text-xs text-muted-foreground"
|
|
172
|
+
aria-hidden="true"
|
|
173
|
+
/>
|
|
174
|
+
<Input
|
|
175
|
+
placeholder={`Search ${label}…`}
|
|
176
|
+
value={searchValue}
|
|
177
|
+
onChange={e => onSearchChange(e.target.value)}
|
|
178
|
+
onKeyDown={e => e.stopPropagation()}
|
|
179
|
+
className="h-7 pl-6 text-xs"
|
|
180
|
+
/>
|
|
181
|
+
{searchValue ? (
|
|
182
|
+
<button
|
|
183
|
+
type="button"
|
|
184
|
+
aria-label="Clear search"
|
|
185
|
+
onClick={() => onSearchChange("")}
|
|
186
|
+
className="absolute right-1.5 top-1/2 -translate-y-1/2 text-muted-foreground transition-colors hover:text-interactive-hover-foreground"
|
|
187
|
+
>
|
|
188
|
+
<i className="fa-light fa-xmark text-xs" aria-hidden="true" />
|
|
189
|
+
</button>
|
|
190
|
+
) : null}
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
<DropdownMenuSeparator />
|
|
194
|
+
|
|
195
|
+
{menu.filterableColumns.length > 0 && (
|
|
196
|
+
<>
|
|
197
|
+
<DropdownMenuSub>
|
|
198
|
+
<DropdownMenuSubTrigger>
|
|
199
|
+
<i className="fa-light fa-filter" aria-hidden="true" />
|
|
200
|
+
Filter by field…
|
|
201
|
+
</DropdownMenuSubTrigger>
|
|
202
|
+
<DropdownMenuSubContent className="max-h-[min(280px,60vh)] overflow-y-auto">
|
|
203
|
+
{menu.filterableColumns.map(col => (
|
|
204
|
+
<DropdownMenuItem key={col.key} onClick={() => menu.onAddFilter(col.key)}>
|
|
205
|
+
{col.label}
|
|
206
|
+
</DropdownMenuItem>
|
|
207
|
+
))}
|
|
208
|
+
</DropdownMenuSubContent>
|
|
209
|
+
</DropdownMenuSub>
|
|
210
|
+
<DropdownMenuSeparator />
|
|
211
|
+
</>
|
|
212
|
+
)}
|
|
213
|
+
|
|
214
|
+
{menu.sortableColumns.length > 0 && (
|
|
215
|
+
<>
|
|
216
|
+
<DropdownMenuSub>
|
|
217
|
+
<DropdownMenuSubTrigger>
|
|
218
|
+
<i className="fa-light fa-arrow-up-arrow-down" aria-hidden="true" />
|
|
219
|
+
Sort by…
|
|
220
|
+
</DropdownMenuSubTrigger>
|
|
221
|
+
<DropdownMenuSubContent className="max-h-[min(320px,60vh)] overflow-y-auto">
|
|
222
|
+
{menu.sortableColumns.map(col => (
|
|
223
|
+
<React.Fragment key={col.key}>
|
|
224
|
+
<DropdownMenuItem onClick={() => menu.onSortByField(col.key, "asc")}>
|
|
225
|
+
<i className="fa-light fa-arrow-up-az" aria-hidden="true" />
|
|
226
|
+
{col.label} — ascending
|
|
227
|
+
</DropdownMenuItem>
|
|
228
|
+
<DropdownMenuItem onClick={() => menu.onSortByField(col.key, "desc")}>
|
|
229
|
+
<i className="fa-light fa-arrow-down-az" aria-hidden="true" />
|
|
230
|
+
{col.label} — descending
|
|
231
|
+
</DropdownMenuItem>
|
|
232
|
+
</React.Fragment>
|
|
233
|
+
))}
|
|
234
|
+
</DropdownMenuSubContent>
|
|
235
|
+
</DropdownMenuSub>
|
|
236
|
+
<DropdownMenuSeparator />
|
|
237
|
+
</>
|
|
238
|
+
)}
|
|
239
|
+
|
|
240
|
+
{menu.groupableColumns.length > 0 && (
|
|
241
|
+
<>
|
|
242
|
+
<DropdownMenuSub>
|
|
243
|
+
<DropdownMenuSubTrigger>
|
|
244
|
+
<i className="fa-light fa-layer-group" aria-hidden="true" />
|
|
245
|
+
Group by…
|
|
246
|
+
</DropdownMenuSubTrigger>
|
|
247
|
+
<DropdownMenuSubContent className="max-h-[min(280px,60vh)] overflow-y-auto">
|
|
248
|
+
{menu.groupableColumns.map(col => (
|
|
249
|
+
<DropdownMenuItem
|
|
250
|
+
key={col.key}
|
|
251
|
+
onClick={() => menu.onToggleGroupBy(col.key)}
|
|
252
|
+
>
|
|
253
|
+
{menu.groupBy === col.key ? (
|
|
254
|
+
<>
|
|
255
|
+
<i className="fa-light fa-check text-xs" aria-hidden="true" />
|
|
256
|
+
Grouped by {col.label}
|
|
257
|
+
</>
|
|
258
|
+
) : (
|
|
259
|
+
<>
|
|
260
|
+
<span className="inline-block w-3" aria-hidden />
|
|
261
|
+
Group by {col.label}
|
|
262
|
+
</>
|
|
263
|
+
)}
|
|
264
|
+
</DropdownMenuItem>
|
|
265
|
+
))}
|
|
266
|
+
</DropdownMenuSubContent>
|
|
267
|
+
</DropdownMenuSub>
|
|
268
|
+
<DropdownMenuSeparator />
|
|
269
|
+
</>
|
|
270
|
+
)}
|
|
271
|
+
|
|
272
|
+
<DropdownMenuItem onClick={menu.onOpenProperties}>
|
|
273
|
+
<i className="fa-light fa-palette" aria-hidden="true" />
|
|
274
|
+
Add conditional rule
|
|
275
|
+
</DropdownMenuItem>
|
|
276
|
+
</DropdownMenuContent>
|
|
277
|
+
</DropdownMenu>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function PlacementsBoardView({
|
|
284
|
+
placements,
|
|
285
|
+
lifecycleTabId,
|
|
286
|
+
boardColumnMenu,
|
|
287
|
+
boardDisplay: boardDisplayProp,
|
|
288
|
+
hiddenColKeys: hiddenColKeysProp,
|
|
289
|
+
conditionalRules,
|
|
290
|
+
boardColumns,
|
|
291
|
+
}: PlacementsBoardViewProps) {
|
|
292
|
+
const router = useRouter()
|
|
293
|
+
|
|
294
|
+
const bd: BoardDisplaySettings = {
|
|
295
|
+
lineCount: boardDisplayProp?.lineCount ?? DEFAULT_DATA_LIST_DISPLAY_OPTIONS.boardLineCount,
|
|
296
|
+
showColumnLabels: boardDisplayProp?.showColumnLabels ?? DEFAULT_DATA_LIST_DISPLAY_OPTIONS.showColumnLabels,
|
|
297
|
+
showColumnCounts: boardDisplayProp?.showColumnCounts ?? DEFAULT_DATA_LIST_DISPLAY_OPTIONS.showBoardColumnCounts,
|
|
298
|
+
newCardAbove: boardDisplayProp?.newCardAbove ?? DEFAULT_DATA_LIST_DISPLAY_OPTIONS.boardNewCardAbove,
|
|
299
|
+
}
|
|
300
|
+
const hiddenColKeys = hiddenColKeysProp ?? new Set<string>()
|
|
301
|
+
|
|
302
|
+
const [phaseSearch, setPhaseSearch] = React.useState<Record<PlacementPhase, string>>({
|
|
303
|
+
upcoming: "",
|
|
304
|
+
ongoing: "",
|
|
305
|
+
completed: "",
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
const byPhase = React.useMemo(() => {
|
|
309
|
+
const map: Record<PlacementPhase, Placement[]> = {
|
|
310
|
+
upcoming: [],
|
|
311
|
+
ongoing: [],
|
|
312
|
+
completed: [],
|
|
313
|
+
}
|
|
314
|
+
for (const p of placements) {
|
|
315
|
+
map[p.placementPhase].push(p)
|
|
316
|
+
}
|
|
317
|
+
return map
|
|
318
|
+
}, [placements])
|
|
319
|
+
|
|
320
|
+
const cardsByPhase = React.useMemo(() => {
|
|
321
|
+
const out: Record<PlacementPhase, Placement[]> = {
|
|
322
|
+
upcoming: [],
|
|
323
|
+
ongoing: [],
|
|
324
|
+
completed: [],
|
|
325
|
+
}
|
|
326
|
+
for (const phase of PHASE_COLUMNS.map(c => c.phase)) {
|
|
327
|
+
const q = phaseSearch[phase]
|
|
328
|
+
out[phase] = byPhase[phase].filter(row => rowMatchesPhaseSearch(row, q))
|
|
329
|
+
}
|
|
330
|
+
return out
|
|
331
|
+
}, [byPhase, phaseSearch])
|
|
332
|
+
|
|
333
|
+
return (
|
|
334
|
+
<div className="px-4 pb-8 pt-2 lg:px-6">
|
|
335
|
+
<p className="text-xs text-muted-foreground mb-4">
|
|
336
|
+
{lifecycleTabId === "all"
|
|
337
|
+
? "Rows grouped by phase (same data as Table view and List view)."
|
|
338
|
+
: `Filtered to ${lifecycleTabId} — cards shown in matching columns only.`}
|
|
339
|
+
</p>
|
|
340
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-3 min-h-[min(480px,calc(100vh-14rem))]">
|
|
341
|
+
{PHASE_COLUMNS.map(col => {
|
|
342
|
+
const rawInPhase = byPhase[col.phase]
|
|
343
|
+
const cards = cardsByPhase[col.phase]
|
|
344
|
+
|
|
345
|
+
return (
|
|
346
|
+
<div
|
|
347
|
+
key={col.phase}
|
|
348
|
+
className="group/board-col flex min-h-0 flex-col rounded-xl border border-border bg-muted/30"
|
|
349
|
+
>
|
|
350
|
+
{boardColumnMenu ? (
|
|
351
|
+
<BoardPhaseColumnHeader
|
|
352
|
+
label={col.label}
|
|
353
|
+
rawCount={rawInPhase.length}
|
|
354
|
+
filteredCount={cards.length}
|
|
355
|
+
searchValue={phaseSearch[col.phase]}
|
|
356
|
+
onSearchChange={v => setPhaseSearch(prev => ({ ...prev, [col.phase]: v }))}
|
|
357
|
+
menu={boardColumnMenu}
|
|
358
|
+
showLabels={bd.showColumnLabels}
|
|
359
|
+
showCounts={bd.showColumnCounts}
|
|
360
|
+
/>
|
|
361
|
+
) : (
|
|
362
|
+
<div className="flex items-center justify-between gap-2 border-b border-border px-3 py-2.5">
|
|
363
|
+
{bd.showColumnLabels ? (
|
|
364
|
+
<p className="min-w-0 truncate text-sm font-semibold text-foreground">{col.label}</p>
|
|
365
|
+
) : (
|
|
366
|
+
<span className="min-w-0 flex-1" aria-hidden />
|
|
367
|
+
)}
|
|
368
|
+
{bd.showColumnCounts ? (
|
|
369
|
+
<Badge
|
|
370
|
+
variant="outline"
|
|
371
|
+
className="inline-flex h-6 min-w-6 shrink-0 items-center justify-center border-0 bg-muted/70 px-2 text-xs font-semibold tabular-nums text-foreground"
|
|
372
|
+
aria-label={`${rawInPhase.length} ${rawInPhase.length === 1 ? "record" : "records"}`}
|
|
373
|
+
>
|
|
374
|
+
{rawInPhase.length}
|
|
375
|
+
</Badge>
|
|
376
|
+
) : null}
|
|
377
|
+
</div>
|
|
378
|
+
)}
|
|
379
|
+
<div className="flex flex-1 flex-col gap-2 overflow-y-auto p-2">
|
|
380
|
+
{bd.newCardAbove ? <BoardNewCardPlaceholder position="above" /> : null}
|
|
381
|
+
{cards.length === 0 ? (
|
|
382
|
+
<p className="px-2 py-6 text-center text-xs text-muted-foreground">No placements</p>
|
|
383
|
+
) : (
|
|
384
|
+
cards.map(row => (
|
|
385
|
+
<BoardPlacementCard
|
|
386
|
+
key={row.id}
|
|
387
|
+
row={row}
|
|
388
|
+
lifecycleTabId={lifecycleTabId}
|
|
389
|
+
hiddenColKeys={hiddenColKeys}
|
|
390
|
+
lineCount={bd.lineCount}
|
|
391
|
+
conditionalRules={conditionalRules}
|
|
392
|
+
boardColumns={boardColumns}
|
|
393
|
+
onOpen={id => router.push(`/data-list/${id}`)}
|
|
394
|
+
/>
|
|
395
|
+
))
|
|
396
|
+
)}
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
)
|
|
400
|
+
})}
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
)
|
|
404
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PlacementsListView — full-width row layout for the data list (vs table grid / board columns).
|
|
5
|
+
* Shares column visibility + lifecycle rules with Table Properties via the same board column model.
|
|
6
|
+
* Long lists use window scroll virtualization (TanStack Virtual) to limit DOM size.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as React from "react"
|
|
10
|
+
import { useRouter } from "next/navigation"
|
|
11
|
+
import { useWindowVirtualizer } from "@tanstack/react-virtual"
|
|
12
|
+
import { cn } from "@/lib/utils"
|
|
13
|
+
import type { Placement } from "@/lib/mock/placements"
|
|
14
|
+
import { StatusBadge } from "@/components/data-list-table-cells"
|
|
15
|
+
import { Badge } from "@/components/ui/badge"
|
|
16
|
+
import { ListPageBoardCard, ListPageBoardCardAvatar } from "@/components/data-views/list-page-board-card"
|
|
17
|
+
import {
|
|
18
|
+
type BoardCardLifecycleTabId,
|
|
19
|
+
isBoardFieldActive,
|
|
20
|
+
scheduleKeysForTab,
|
|
21
|
+
} from "@/lib/placement-board-card-layout"
|
|
22
|
+
import { getConditionalRowBackground } from "@/lib/conditional-rule-match"
|
|
23
|
+
import type { ConditionalRule } from "@/components/table-properties/types"
|
|
24
|
+
import type { ColumnDef } from "@/components/data-table/types"
|
|
25
|
+
|
|
26
|
+
/** Above this count, the list is virtualized against the window scroll. */
|
|
27
|
+
const VIRTUAL_ROWS_THRESHOLD = 80
|
|
28
|
+
/** Initial row height guess (px); `measureElement` refines for variable content. */
|
|
29
|
+
const ESTIMATE_ROW_PX = 100
|
|
30
|
+
|
|
31
|
+
function scheduleSummary(row: Placement, tab: BoardCardLifecycleTabId): string | null {
|
|
32
|
+
switch (tab) {
|
|
33
|
+
case "all":
|
|
34
|
+
return [row.start, row.duration].filter(Boolean).join(" · ") || null
|
|
35
|
+
case "upcoming":
|
|
36
|
+
return row.daysUntilStart > 0
|
|
37
|
+
? `${row.start} · Starts in ${row.daysUntilStart} days`
|
|
38
|
+
: row.start
|
|
39
|
+
case "ongoing":
|
|
40
|
+
return `${row.progressWeeksDone} / ${row.progressWeeksTotal} wks · Ends ${row.endDate}`
|
|
41
|
+
case "completed":
|
|
42
|
+
return [row.completionDate, row.finalStatus].filter(v => v && v !== "—").join(" · ") || null
|
|
43
|
+
default:
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function PlacementListRowContent({
|
|
49
|
+
row,
|
|
50
|
+
tab,
|
|
51
|
+
hiddenColKeys,
|
|
52
|
+
boardColumns,
|
|
53
|
+
conditionalRules,
|
|
54
|
+
onOpen,
|
|
55
|
+
}: {
|
|
56
|
+
row: Placement
|
|
57
|
+
tab: BoardCardLifecycleTabId
|
|
58
|
+
hiddenColKeys: Set<string>
|
|
59
|
+
boardColumns: ColumnDef<Placement>[]
|
|
60
|
+
conditionalRules: ConditionalRule[] | undefined
|
|
61
|
+
onOpen: (id: number) => void
|
|
62
|
+
}) {
|
|
63
|
+
const ruleBg = getConditionalRowBackground(row, conditionalRules)
|
|
64
|
+
const showStudent = isBoardFieldActive("student", tab, hiddenColKeys, boardColumns)
|
|
65
|
+
const showStatus = isBoardFieldActive("status", tab, hiddenColKeys, boardColumns)
|
|
66
|
+
const showSite = isBoardFieldActive("site", tab, hiddenColKeys, boardColumns)
|
|
67
|
+
const showSpec = isBoardFieldActive("specialization", tab, hiddenColKeys, boardColumns)
|
|
68
|
+
const showInternship = isBoardFieldActive("internship", tab, hiddenColKeys, boardColumns)
|
|
69
|
+
const sk = scheduleKeysForTab(tab)
|
|
70
|
+
const showSchedule = sk.some(k => isBoardFieldActive(k, tab, hiddenColKeys, boardColumns))
|
|
71
|
+
const schedule = showSchedule ? scheduleSummary(row, tab) : null
|
|
72
|
+
|
|
73
|
+
const title = showStudent ? row.student : `Placement ${row.id}`
|
|
74
|
+
|
|
75
|
+
const leading = showStudent ? (
|
|
76
|
+
<ListPageBoardCardAvatar initials={row.initials} className="size-9" />
|
|
77
|
+
) : (
|
|
78
|
+
<span className="size-9 shrink-0 rounded-full bg-muted/80" aria-hidden />
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
const rowEnd = showStatus ? (
|
|
82
|
+
<div className="flex shrink-0 items-center gap-2 pt-0.5">
|
|
83
|
+
<StatusBadge status={row.status} surface="board" />
|
|
84
|
+
<i className="fa-light fa-chevron-right text-xs text-muted-foreground" aria-hidden />
|
|
85
|
+
</div>
|
|
86
|
+
) : (
|
|
87
|
+
<i className="fa-light fa-chevron-right mt-1 shrink-0 text-xs text-muted-foreground" aria-hidden />
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<ListPageBoardCard
|
|
92
|
+
layout="row"
|
|
93
|
+
leading={leading}
|
|
94
|
+
rowEnd={rowEnd}
|
|
95
|
+
isNew={row.isNew}
|
|
96
|
+
style={ruleBg ? { background: ruleBg } : undefined}
|
|
97
|
+
onClick={() => onOpen(row.id)}
|
|
98
|
+
>
|
|
99
|
+
<div className="space-y-1">
|
|
100
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
101
|
+
<span className="text-sm font-semibold text-foreground">{title}</span>
|
|
102
|
+
{row.isNew ? (
|
|
103
|
+
<Badge variant="secondary" className="h-5 px-1.5 text-xs font-medium">
|
|
104
|
+
New
|
|
105
|
+
</Badge>
|
|
106
|
+
) : null}
|
|
107
|
+
</div>
|
|
108
|
+
{showSite ? (
|
|
109
|
+
<p className="text-xs text-foreground/90">
|
|
110
|
+
<span className="font-medium">{row.site}</span>
|
|
111
|
+
{row.siteAddress ? (
|
|
112
|
+
<span className="text-muted-foreground"> · {row.siteAddress}</span>
|
|
113
|
+
) : null}
|
|
114
|
+
</p>
|
|
115
|
+
) : null}
|
|
116
|
+
{(showSpec || showInternship) ? (
|
|
117
|
+
<p className="text-xs text-muted-foreground">
|
|
118
|
+
{[showSpec ? row.specialization : null, showInternship ? row.internship : null]
|
|
119
|
+
.filter(Boolean)
|
|
120
|
+
.join(" · ")}
|
|
121
|
+
</p>
|
|
122
|
+
) : null}
|
|
123
|
+
{schedule ? <p className="text-xs text-muted-foreground tabular-nums">{schedule}</p> : null}
|
|
124
|
+
</div>
|
|
125
|
+
</ListPageBoardCard>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function PlacementListRow({
|
|
130
|
+
row,
|
|
131
|
+
tab,
|
|
132
|
+
hiddenColKeys,
|
|
133
|
+
boardColumns,
|
|
134
|
+
conditionalRules,
|
|
135
|
+
onOpen,
|
|
136
|
+
}: {
|
|
137
|
+
row: Placement
|
|
138
|
+
tab: BoardCardLifecycleTabId
|
|
139
|
+
hiddenColKeys: Set<string>
|
|
140
|
+
boardColumns: ColumnDef<Placement>[]
|
|
141
|
+
conditionalRules: ConditionalRule[] | undefined
|
|
142
|
+
onOpen: (id: number) => void
|
|
143
|
+
}) {
|
|
144
|
+
return (
|
|
145
|
+
<li>
|
|
146
|
+
<PlacementListRowContent
|
|
147
|
+
row={row}
|
|
148
|
+
tab={tab}
|
|
149
|
+
hiddenColKeys={hiddenColKeys}
|
|
150
|
+
boardColumns={boardColumns}
|
|
151
|
+
conditionalRules={conditionalRules}
|
|
152
|
+
onOpen={onOpen}
|
|
153
|
+
/>
|
|
154
|
+
</li>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function PlacementsListViewVirtualized({
|
|
159
|
+
rows,
|
|
160
|
+
lifecycleTabId,
|
|
161
|
+
hiddenColKeys,
|
|
162
|
+
boardColumns,
|
|
163
|
+
conditionalRules,
|
|
164
|
+
onOpen,
|
|
165
|
+
}: {
|
|
166
|
+
rows: Placement[]
|
|
167
|
+
lifecycleTabId: BoardCardLifecycleTabId
|
|
168
|
+
hiddenColKeys: Set<string>
|
|
169
|
+
boardColumns: ColumnDef<Placement>[]
|
|
170
|
+
conditionalRules: ConditionalRule[] | undefined
|
|
171
|
+
onOpen: (id: number) => void
|
|
172
|
+
}) {
|
|
173
|
+
const anchorRef = React.useRef<HTMLDivElement>(null)
|
|
174
|
+
const [scrollMargin, setScrollMargin] = React.useState(0)
|
|
175
|
+
|
|
176
|
+
const updateScrollMargin = React.useCallback(() => {
|
|
177
|
+
const el = anchorRef.current
|
|
178
|
+
if (!el) return
|
|
179
|
+
setScrollMargin(el.getBoundingClientRect().top + window.scrollY)
|
|
180
|
+
}, [])
|
|
181
|
+
|
|
182
|
+
React.useLayoutEffect(() => {
|
|
183
|
+
updateScrollMargin()
|
|
184
|
+
window.addEventListener("resize", updateScrollMargin)
|
|
185
|
+
return () => window.removeEventListener("resize", updateScrollMargin)
|
|
186
|
+
}, [updateScrollMargin, rows.length, lifecycleTabId])
|
|
187
|
+
|
|
188
|
+
const virtualizer = useWindowVirtualizer({
|
|
189
|
+
count: rows.length,
|
|
190
|
+
estimateSize: () => ESTIMATE_ROW_PX,
|
|
191
|
+
overscan: 8,
|
|
192
|
+
scrollMargin,
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<div ref={anchorRef} className="px-4 pb-8 pt-2 lg:px-6">
|
|
197
|
+
<ul
|
|
198
|
+
role="list"
|
|
199
|
+
className="relative m-0 w-full list-none p-0"
|
|
200
|
+
style={{ height: virtualizer.getTotalSize() }}
|
|
201
|
+
>
|
|
202
|
+
{virtualizer.getVirtualItems().map(vr => {
|
|
203
|
+
const row = rows[vr.index]
|
|
204
|
+
if (!row) return null
|
|
205
|
+
return (
|
|
206
|
+
<li
|
|
207
|
+
key={vr.key}
|
|
208
|
+
data-index={vr.index}
|
|
209
|
+
ref={virtualizer.measureElement}
|
|
210
|
+
className="absolute left-0 top-0 w-full pb-2"
|
|
211
|
+
style={{ transform: `translateY(${vr.start}px)` }}
|
|
212
|
+
>
|
|
213
|
+
<PlacementListRowContent
|
|
214
|
+
row={row}
|
|
215
|
+
tab={lifecycleTabId}
|
|
216
|
+
hiddenColKeys={hiddenColKeys}
|
|
217
|
+
boardColumns={boardColumns}
|
|
218
|
+
conditionalRules={conditionalRules}
|
|
219
|
+
onOpen={onOpen}
|
|
220
|
+
/>
|
|
221
|
+
</li>
|
|
222
|
+
)
|
|
223
|
+
})}
|
|
224
|
+
</ul>
|
|
225
|
+
</div>
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export interface PlacementsListViewProps {
|
|
230
|
+
rows: Placement[]
|
|
231
|
+
lifecycleTabId: BoardCardLifecycleTabId
|
|
232
|
+
hiddenColKeys: Set<string>
|
|
233
|
+
boardColumns: ColumnDef<Placement>[]
|
|
234
|
+
conditionalRules?: ConditionalRule[]
|
|
235
|
+
emptyCopy: string
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function PlacementsListView({
|
|
239
|
+
rows,
|
|
240
|
+
lifecycleTabId,
|
|
241
|
+
hiddenColKeys,
|
|
242
|
+
boardColumns,
|
|
243
|
+
conditionalRules,
|
|
244
|
+
emptyCopy,
|
|
245
|
+
}: PlacementsListViewProps) {
|
|
246
|
+
const router = useRouter()
|
|
247
|
+
const onOpen = React.useCallback((id: number) => router.push(`/data-list/${id}`), [router])
|
|
248
|
+
|
|
249
|
+
if (rows.length === 0) {
|
|
250
|
+
return (
|
|
251
|
+
<div className="px-4 py-16 text-center lg:px-6">
|
|
252
|
+
<p className="text-sm text-muted-foreground">{emptyCopy}</p>
|
|
253
|
+
</div>
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (rows.length >= VIRTUAL_ROWS_THRESHOLD) {
|
|
258
|
+
return (
|
|
259
|
+
<PlacementsListViewVirtualized
|
|
260
|
+
rows={rows}
|
|
261
|
+
lifecycleTabId={lifecycleTabId}
|
|
262
|
+
hiddenColKeys={hiddenColKeys}
|
|
263
|
+
boardColumns={boardColumns}
|
|
264
|
+
conditionalRules={conditionalRules}
|
|
265
|
+
onOpen={onOpen}
|
|
266
|
+
/>
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<ul className="flex list-none flex-col gap-2 px-4 pb-8 pt-2 lg:px-6">
|
|
272
|
+
{rows.map(row => (
|
|
273
|
+
<PlacementListRow
|
|
274
|
+
key={row.id}
|
|
275
|
+
row={row}
|
|
276
|
+
tab={lifecycleTabId}
|
|
277
|
+
hiddenColKeys={hiddenColKeys}
|
|
278
|
+
boardColumns={boardColumns}
|
|
279
|
+
conditionalRules={conditionalRules}
|
|
280
|
+
onOpen={onOpen}
|
|
281
|
+
/>
|
|
282
|
+
))}
|
|
283
|
+
</ul>
|
|
284
|
+
)
|
|
285
|
+
}
|