@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
|
@@ -31,11 +31,41 @@ import {
|
|
|
31
31
|
} from "@/components/ui/dropdown-menu"
|
|
32
32
|
import { Tip } from "@/components/ui/tip"
|
|
33
33
|
import { KeyMetrics } from "@/components/key-metrics"
|
|
34
|
+
import {
|
|
35
|
+
ResizableHandle,
|
|
36
|
+
ResizablePanel,
|
|
37
|
+
ResizablePanelGroup,
|
|
38
|
+
} from "@/components/ui/resizable"
|
|
39
|
+
import {
|
|
40
|
+
Tooltip,
|
|
41
|
+
TooltipContent,
|
|
42
|
+
TooltipTrigger,
|
|
43
|
+
} from "@/components/ui/tooltip"
|
|
44
|
+
import { ListPageSplitHubChrome } from "@/components/data-views/list-page-split-hub-chrome"
|
|
45
|
+
import {
|
|
46
|
+
LIST_PAGE_SPLIT_MILLER_DETAIL_PANEL_CLASS,
|
|
47
|
+
LIST_PAGE_SPLIT_RESIZABLE_HANDLE_CLASS,
|
|
48
|
+
LIST_PAGE_SPLIT_MILLER_COLUMN_PANEL_CLASS,
|
|
49
|
+
} from "@/components/data-views/list-page-split-hub-tokens"
|
|
50
|
+
import { ListPageTreeColumnHeader } from "@/components/data-views/list-page-tree-column-header"
|
|
34
51
|
import { QuestionBankBoardView, QUESTION_BANK_BOARD_GROUP_OPTIONS } from "@/components/question-bank-board-view"
|
|
35
52
|
import { QuestionBankListView } from "@/components/question-bank-list-view"
|
|
53
|
+
import { QuestionBankOsFolderView } from "@/components/question-bank-os-folder-view"
|
|
54
|
+
import { QuestionBankNewFolderSheet } from "@/components/question-bank-new-folder-sheet"
|
|
55
|
+
import { FolderDetailsShell } from "@/components/folder-details-shell"
|
|
56
|
+
import { HubTreePanelView } from "@/components/hub-tree-panel-view"
|
|
57
|
+
import { OsFolderGlyph } from "@/components/data-views/os-folder-glyph"
|
|
58
|
+
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
|
59
|
+
import { initialsFromDisplayName } from "@/lib/initials-from-name"
|
|
60
|
+
import { cn } from "@/lib/utils"
|
|
36
61
|
import { CHART_KBD_ACTIVE_BAR } from "@/lib/chart-keyboard-selection"
|
|
37
62
|
import type { QuestionBankDifficulty, QuestionBankItem, QuestionBankType } from "@/lib/mock/question-bank"
|
|
63
|
+
import { newFolderId, type QuestionBankFolder, QUESTION_BANK_FOLDER_COLOR_STYLES, QUESTION_BANK_FOLDER_ICON_COLORS } from "@/lib/mock/question-bank-folders"
|
|
38
64
|
import { questionBankKpiInsight, questionBankKpiMetrics } from "@/lib/mock/question-bank-kpi"
|
|
65
|
+
import {
|
|
66
|
+
filterQuestionBankItemsByNav,
|
|
67
|
+
type QuestionBankNavState,
|
|
68
|
+
} from "@/lib/question-bank-nav"
|
|
39
69
|
import {
|
|
40
70
|
QUESTION_BANK_STATUS_BADGE_CLASS,
|
|
41
71
|
QUESTION_BANK_STATUS_ICON,
|
|
@@ -62,6 +92,17 @@ const BAR_CFG: ChartConfig = {
|
|
|
62
92
|
count: { label: "Questions", color: "var(--color-chart-2)" },
|
|
63
93
|
}
|
|
64
94
|
|
|
95
|
+
function newQuestionBankItemId() {
|
|
96
|
+
return `q-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Folder id to use when adding a question from the root column (`parentId` null). */
|
|
100
|
+
function defaultFolderIdForColumnParent(parentId: string | null, folders: QuestionBankFolder[]): string | null {
|
|
101
|
+
if (parentId !== null) return parentId
|
|
102
|
+
const roots = [...folders].filter(f => f.parentId === null).sort((a, b) => a.name.localeCompare(b.name))
|
|
103
|
+
return roots[0]?.id ?? null
|
|
104
|
+
}
|
|
105
|
+
|
|
65
106
|
function uniqueTopics(items: QuestionBankItem[]) {
|
|
66
107
|
return [...new Set(items.map(i => i.topic))].sort().map(t => ({ value: t, label: t }))
|
|
67
108
|
}
|
|
@@ -328,6 +369,114 @@ function QuestionsByStatusChart({ rows }: { rows: QuestionBankItem[] }) {
|
|
|
328
369
|
)
|
|
329
370
|
}
|
|
330
371
|
|
|
372
|
+
function QuestionBankFinderListRow({
|
|
373
|
+
row,
|
|
374
|
+
isSelected,
|
|
375
|
+
compact,
|
|
376
|
+
}: {
|
|
377
|
+
row: QuestionBankItem
|
|
378
|
+
isSelected: boolean
|
|
379
|
+
compact?: boolean
|
|
380
|
+
}) {
|
|
381
|
+
const initials = initialsFromDisplayName(row.author)
|
|
382
|
+
return (
|
|
383
|
+
<div
|
|
384
|
+
className={cn(
|
|
385
|
+
"flex w-full items-center px-3 py-2",
|
|
386
|
+
compact ? "gap-2 py-1.5 pl-2 pr-2.5" : "gap-3",
|
|
387
|
+
)}
|
|
388
|
+
>
|
|
389
|
+
<Avatar className={cn("shrink-0", compact ? "size-6" : "size-8")}>
|
|
390
|
+
<AvatarFallback
|
|
391
|
+
className={cn(
|
|
392
|
+
"font-semibold",
|
|
393
|
+
compact ? "text-[10px]" : "text-[11px]",
|
|
394
|
+
isSelected ? "bg-background/25 text-accent-foreground" : "bg-brand/15 text-brand",
|
|
395
|
+
)}
|
|
396
|
+
>
|
|
397
|
+
{initials}
|
|
398
|
+
</AvatarFallback>
|
|
399
|
+
</Avatar>
|
|
400
|
+
<div className="min-w-0 flex-1">
|
|
401
|
+
<p className={cn("line-clamp-2 font-medium leading-tight", compact ? "text-[11px]" : "text-xs")}>
|
|
402
|
+
{row.stem}
|
|
403
|
+
</p>
|
|
404
|
+
<p
|
|
405
|
+
className={cn(
|
|
406
|
+
"mt-0.5 truncate text-[11px] leading-tight",
|
|
407
|
+
isSelected ? "text-accent-foreground/85" : "text-muted-foreground",
|
|
408
|
+
)}
|
|
409
|
+
>
|
|
410
|
+
{row.topic} · {row.author}
|
|
411
|
+
</p>
|
|
412
|
+
</div>
|
|
413
|
+
{!isSelected && (
|
|
414
|
+
<ListHubStatusBadge
|
|
415
|
+
surface="board"
|
|
416
|
+
label={QUESTION_BANK_STATUS_LABEL[row.status]}
|
|
417
|
+
tintClassName={QUESTION_BANK_STATUS_BADGE_CLASS[row.status]}
|
|
418
|
+
icon={QUESTION_BANK_STATUS_ICON[row.status]}
|
|
419
|
+
/>
|
|
420
|
+
)}
|
|
421
|
+
</div>
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function QuestionBankFinderDetail({ row }: { row: QuestionBankItem }) {
|
|
426
|
+
return (
|
|
427
|
+
<div className="flex h-full min-h-0 flex-col">
|
|
428
|
+
<div className="flex shrink-0 flex-col gap-2 border-b border-border px-5 py-4">
|
|
429
|
+
<h2 className="line-clamp-4 text-base font-semibold leading-tight text-foreground">{row.stem}</h2>
|
|
430
|
+
<ListHubStatusBadge
|
|
431
|
+
surface="board"
|
|
432
|
+
label={QUESTION_BANK_STATUS_LABEL[row.status]}
|
|
433
|
+
tintClassName={QUESTION_BANK_STATUS_BADGE_CLASS[row.status]}
|
|
434
|
+
icon={QUESTION_BANK_STATUS_ICON[row.status]}
|
|
435
|
+
/>
|
|
436
|
+
</div>
|
|
437
|
+
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
|
|
438
|
+
<dl className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
439
|
+
<div className="flex flex-col gap-0.5">
|
|
440
|
+
<dt className="flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
441
|
+
<i className="fa-light fa-layer-group text-xs" aria-hidden="true" />
|
|
442
|
+
Topic
|
|
443
|
+
</dt>
|
|
444
|
+
<dd className="text-xs text-foreground">{row.topic}</dd>
|
|
445
|
+
</div>
|
|
446
|
+
<div className="flex flex-col gap-0.5">
|
|
447
|
+
<dt className="flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
448
|
+
<i className="fa-light fa-list-check text-xs" aria-hidden="true" />
|
|
449
|
+
Type
|
|
450
|
+
</dt>
|
|
451
|
+
<dd className="text-xs text-foreground">{TYPE_LABEL[row.type]}</dd>
|
|
452
|
+
</div>
|
|
453
|
+
<div className="flex flex-col gap-0.5">
|
|
454
|
+
<dt className="flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
455
|
+
<i className="fa-light fa-signal text-xs" aria-hidden="true" />
|
|
456
|
+
Difficulty
|
|
457
|
+
</dt>
|
|
458
|
+
<dd className="text-xs text-foreground">{DIFFICULTY_LABEL[row.difficulty]}</dd>
|
|
459
|
+
</div>
|
|
460
|
+
<div className="flex flex-col gap-0.5">
|
|
461
|
+
<dt className="flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
462
|
+
<i className="fa-light fa-user text-xs" aria-hidden="true" />
|
|
463
|
+
Author
|
|
464
|
+
</dt>
|
|
465
|
+
<dd className="text-xs text-foreground">{row.author}</dd>
|
|
466
|
+
</div>
|
|
467
|
+
<div className="flex flex-col gap-0.5 sm:col-span-2">
|
|
468
|
+
<dt className="flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
469
|
+
<i className="fa-light fa-calendar-days text-xs" aria-hidden="true" />
|
|
470
|
+
Updated
|
|
471
|
+
</dt>
|
|
472
|
+
<dd className="text-xs tabular-nums text-foreground">{row.updatedAt}</dd>
|
|
473
|
+
</div>
|
|
474
|
+
</dl>
|
|
475
|
+
</div>
|
|
476
|
+
</div>
|
|
477
|
+
)
|
|
478
|
+
}
|
|
479
|
+
|
|
331
480
|
function QuestionsByTopicChart({ rows }: { rows: QuestionBankItem[] }) {
|
|
332
481
|
const data = React.useMemo(() => aggregateByTopic(rows), [rows])
|
|
333
482
|
if (rows.length === 0) {
|
|
@@ -403,13 +552,335 @@ function QuestionBankDashboardSimple({ rows }: { rows: QuestionBankItem[] }) {
|
|
|
403
552
|
)
|
|
404
553
|
}
|
|
405
554
|
|
|
555
|
+
interface HubFolderColumnsPanelProps {
|
|
556
|
+
folders: QuestionBankFolder[]
|
|
557
|
+
rows: QuestionBankItem[]
|
|
558
|
+
panelRenderDetail: (row: QuestionBankItem) => React.ReactNode
|
|
559
|
+
onAddFolder: (parentId: string | null) => void
|
|
560
|
+
onAddQuestion: (parentId: string | null) => void
|
|
561
|
+
onCustomizeFolder?: (folder: QuestionBankFolder) => void
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
type HierarchyItem = QuestionBankFolder | QuestionBankItem
|
|
565
|
+
|
|
566
|
+
function isFolder(item: HierarchyItem): item is QuestionBankFolder {
|
|
567
|
+
return 'parentId' in item
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function isQuestion(item: HierarchyItem): item is QuestionBankItem {
|
|
571
|
+
return 'stem' in item
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** **Panel view** — multi-column folder explorer + optional detail column (Finder-style). */
|
|
575
|
+
function HubFolderColumnsPanel({
|
|
576
|
+
folders,
|
|
577
|
+
rows,
|
|
578
|
+
panelRenderDetail,
|
|
579
|
+
onAddFolder,
|
|
580
|
+
onAddQuestion,
|
|
581
|
+
onCustomizeFolder,
|
|
582
|
+
}: HubFolderColumnsPanelProps) {
|
|
583
|
+
// Track the selected path through the hierarchy
|
|
584
|
+
// Initialize with first folder selected by default
|
|
585
|
+
const [selectedPath, setSelectedPath] = React.useState<HierarchyItem[]>(() => {
|
|
586
|
+
const rootFolders = folders
|
|
587
|
+
.filter(f => f.parentId === null)
|
|
588
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
589
|
+
if (rootFolders.length > 0) {
|
|
590
|
+
return [rootFolders[0]]
|
|
591
|
+
}
|
|
592
|
+
return []
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
// Track if this is the first render for auto-selection
|
|
596
|
+
const isFirstRenderRef = React.useRef(true)
|
|
597
|
+
|
|
598
|
+
// Get root items (top-level folders)
|
|
599
|
+
const rootFolders = React.useMemo(() => {
|
|
600
|
+
return folders
|
|
601
|
+
.filter(f => f.parentId === null)
|
|
602
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
603
|
+
}, [folders])
|
|
604
|
+
|
|
605
|
+
// Handle selection at any depth
|
|
606
|
+
const handleSelect = (item: HierarchyItem, depth: number) => {
|
|
607
|
+
setSelectedPath(prev => [...prev.slice(0, depth), item])
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Auto-select first item at each level (only on first render)
|
|
611
|
+
React.useEffect(() => {
|
|
612
|
+
// Only auto-select if we're at a folder in the path and this is the first render
|
|
613
|
+
if (isFirstRenderRef.current && selectedPath.length > 0) {
|
|
614
|
+
const lastItem = selectedPath[selectedPath.length - 1]
|
|
615
|
+
if (isFolder(lastItem)) {
|
|
616
|
+
const folder = lastItem as QuestionBankFolder
|
|
617
|
+
// Get the items in this folder
|
|
618
|
+
const subfolders = folders.filter(f => f.parentId === folder.id).sort((a, b) => a.name.localeCompare(b.name))
|
|
619
|
+
const questionsInFolder = rows.filter(r => r.folderId === folder.id)
|
|
620
|
+
const items: HierarchyItem[] = [...subfolders, ...questionsInFolder]
|
|
621
|
+
|
|
622
|
+
// If there are items and nothing is selected at the next level, select the first item
|
|
623
|
+
if (items.length > 0 && !selectedPath[selectedPath.length + 1]) {
|
|
624
|
+
setSelectedPath(prev => [...prev, items[0]])
|
|
625
|
+
isFirstRenderRef.current = false
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}, [])
|
|
630
|
+
|
|
631
|
+
// Build columns dynamically based on selected path
|
|
632
|
+
const columns: Array<{ items: HierarchyItem[]; depth: number; parentId?: string | null }> = React.useMemo(() => {
|
|
633
|
+
const cols: Array<{ items: HierarchyItem[]; depth: number; parentId?: string | null }> = [
|
|
634
|
+
{ items: rootFolders, depth: 0, parentId: null },
|
|
635
|
+
]
|
|
636
|
+
|
|
637
|
+
// For each selected folder in the path, add a column with its children
|
|
638
|
+
for (let i = 0; i < selectedPath.length; i++) {
|
|
639
|
+
const item = selectedPath[i]
|
|
640
|
+
if (isFolder(item)) {
|
|
641
|
+
// Get subfolders
|
|
642
|
+
const subfolders = folders
|
|
643
|
+
.filter(f => f.parentId === item.id)
|
|
644
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
645
|
+
|
|
646
|
+
// Get questions in this folder
|
|
647
|
+
const questionsInFolder = rows.filter(r => r.folderId === item.id)
|
|
648
|
+
|
|
649
|
+
// Combine folders and questions
|
|
650
|
+
const items: HierarchyItem[] = [...subfolders, ...questionsInFolder]
|
|
651
|
+
|
|
652
|
+
if (items.length > 0) {
|
|
653
|
+
cols.push({ items, depth: i + 1, parentId: item.id })
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return cols
|
|
659
|
+
}, [selectedPath, rootFolders, folders, rows])
|
|
660
|
+
|
|
661
|
+
const selectedLeaf = selectedPath.length > 0 ? selectedPath.at(-1)! : null
|
|
662
|
+
const selectedQuestion =
|
|
663
|
+
selectedLeaf && isQuestion(selectedLeaf) ? (selectedLeaf as QuestionBankItem) : null
|
|
664
|
+
const selectedFolderLeaf =
|
|
665
|
+
selectedLeaf && isFolder(selectedLeaf) ? (selectedLeaf as QuestionBankFolder) : null
|
|
666
|
+
|
|
667
|
+
return (
|
|
668
|
+
<ResizablePanelGroup
|
|
669
|
+
direction="horizontal"
|
|
670
|
+
className="flex h-full min-h-0 w-full flex-1 overflow-hidden"
|
|
671
|
+
>
|
|
672
|
+
{/* Render all columns with handles between them */}
|
|
673
|
+
{columns.map(({ items, depth, parentId }, columnIdx) => (
|
|
674
|
+
<React.Fragment key={`col-${depth}`}>
|
|
675
|
+
{columnIdx > 0 && (
|
|
676
|
+
<ResizableHandle withHandle className={LIST_PAGE_SPLIT_RESIZABLE_HANDLE_CLASS} />
|
|
677
|
+
)}
|
|
678
|
+
<ResizablePanel
|
|
679
|
+
id={`col-${depth}`}
|
|
680
|
+
defaultSize={columnIdx === 0 ? 35 : columnIdx === 1 ? 35 : 30}
|
|
681
|
+
minSize={15}
|
|
682
|
+
className={LIST_PAGE_SPLIT_MILLER_COLUMN_PANEL_CLASS}
|
|
683
|
+
>
|
|
684
|
+
<ListPageTreeColumnHeader
|
|
685
|
+
title={
|
|
686
|
+
depth === 0
|
|
687
|
+
? "Categories"
|
|
688
|
+
: selectedPath[depth - 1] && isFolder(selectedPath[depth - 1])
|
|
689
|
+
? (selectedPath[depth - 1] as QuestionBankFolder).name
|
|
690
|
+
: "Items"
|
|
691
|
+
}
|
|
692
|
+
trailing={
|
|
693
|
+
<>
|
|
694
|
+
<span className="shrink-0 text-xs font-medium text-muted-foreground tabular-nums">
|
|
695
|
+
{items.length}
|
|
696
|
+
</span>
|
|
697
|
+
{depth < columns.length - 1 && items.length > 0 ? (
|
|
698
|
+
<div className="flex shrink-0 items-center gap-0.5">
|
|
699
|
+
<Tooltip>
|
|
700
|
+
<TooltipTrigger asChild>
|
|
701
|
+
<Button
|
|
702
|
+
size="icon-sm"
|
|
703
|
+
variant="ghost"
|
|
704
|
+
onClick={() => onAddFolder(parentId ?? null)}
|
|
705
|
+
aria-label="Add folder"
|
|
706
|
+
>
|
|
707
|
+
<i className="fa-light fa-folder-plus text-xs" aria-hidden="true" />
|
|
708
|
+
</Button>
|
|
709
|
+
</TooltipTrigger>
|
|
710
|
+
<TooltipContent side="top" sideOffset={4}>
|
|
711
|
+
Add folder
|
|
712
|
+
</TooltipContent>
|
|
713
|
+
</Tooltip>
|
|
714
|
+
<Tooltip>
|
|
715
|
+
<TooltipTrigger asChild>
|
|
716
|
+
<Button
|
|
717
|
+
size="icon-sm"
|
|
718
|
+
variant="ghost"
|
|
719
|
+
onClick={() => onAddQuestion(parentId ?? null)}
|
|
720
|
+
aria-label="Add question"
|
|
721
|
+
>
|
|
722
|
+
<i className="fa-light fa-plus text-xs" aria-hidden="true" />
|
|
723
|
+
</Button>
|
|
724
|
+
</TooltipTrigger>
|
|
725
|
+
<TooltipContent side="top" sideOffset={4}>
|
|
726
|
+
Add question
|
|
727
|
+
</TooltipContent>
|
|
728
|
+
</Tooltip>
|
|
729
|
+
</div>
|
|
730
|
+
) : null}
|
|
731
|
+
</>
|
|
732
|
+
}
|
|
733
|
+
/>
|
|
734
|
+
|
|
735
|
+
{/* Scrollable Items List */}
|
|
736
|
+
<div className="min-h-0 flex-1 overflow-y-auto py-1">
|
|
737
|
+
{items.map(item => {
|
|
738
|
+
const isSelected = selectedPath[depth]?.id === item.id
|
|
739
|
+
const isFolder_ = isFolder(item)
|
|
740
|
+
const folder = isFolder_ ? item : null
|
|
741
|
+
const question = isQuestion(item) ? item : null
|
|
742
|
+
|
|
743
|
+
// Get count for folders
|
|
744
|
+
const subfolderCount = isFolder_
|
|
745
|
+
? folders.filter(f => f.parentId === item.id).length
|
|
746
|
+
: 0
|
|
747
|
+
const questionCount = isFolder_
|
|
748
|
+
? rows.filter(r => r.folderId === item.id).length
|
|
749
|
+
: 0
|
|
750
|
+
const itemCount = subfolderCount + questionCount
|
|
751
|
+
|
|
752
|
+
return (
|
|
753
|
+
<div
|
|
754
|
+
key={item.id}
|
|
755
|
+
className="group flex items-center hover:bg-muted/50"
|
|
756
|
+
>
|
|
757
|
+
<button
|
|
758
|
+
onClick={() => handleSelect(item, depth)}
|
|
759
|
+
className={cn(
|
|
760
|
+
"flex flex-1 items-center gap-3 px-3 py-2 text-left text-sm transition-colors duration-75",
|
|
761
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
|
|
762
|
+
isSelected
|
|
763
|
+
? "bg-accent text-accent-foreground"
|
|
764
|
+
: "text-foreground",
|
|
765
|
+
// Apply folder background color if it's a folder and not selected, but NOT in the first column
|
|
766
|
+
isFolder_ && !isSelected && folder?.colorKey && depth > 0
|
|
767
|
+
? QUESTION_BANK_FOLDER_COLOR_STYLES[folder.colorKey]?.tile
|
|
768
|
+
: "",
|
|
769
|
+
)}
|
|
770
|
+
aria-selected={isSelected}
|
|
771
|
+
role="option"
|
|
772
|
+
>
|
|
773
|
+
{/* Icon - show for folders and questions */}
|
|
774
|
+
{isFolder_ ? (
|
|
775
|
+
<i className={cn(
|
|
776
|
+
"fa-folder text-sm shrink-0",
|
|
777
|
+
isSelected ? "fa-solid" : "fa-light",
|
|
778
|
+
// Apply folder color from customization (for both selected and unselected)
|
|
779
|
+
folder?.colorKey && QUESTION_BANK_FOLDER_ICON_COLORS[folder.colorKey]
|
|
780
|
+
)} aria-hidden="true" />
|
|
781
|
+
) : (
|
|
782
|
+
<i className={cn("fa-file text-sm shrink-0", isSelected ? "fa-solid" : "fa-light")} aria-hidden="true" />
|
|
783
|
+
)}
|
|
784
|
+
|
|
785
|
+
{/* Name */}
|
|
786
|
+
<span className={cn(
|
|
787
|
+
"min-w-0 flex-1 truncate leading-tight",
|
|
788
|
+
isSelected && "font-medium"
|
|
789
|
+
)}>
|
|
790
|
+
{isFolder_ ? folder?.name : question?.stem}
|
|
791
|
+
</span>
|
|
792
|
+
|
|
793
|
+
{/* Count or metadata */}
|
|
794
|
+
<span className={cn(
|
|
795
|
+
"shrink-0 tabular-nums text-xs ml-auto",
|
|
796
|
+
isSelected ? "text-accent-foreground/70" : "text-muted-foreground",
|
|
797
|
+
)}>
|
|
798
|
+
{isFolder_ ? itemCount : (question?.type === 'multiple_choice' ? 'MCQ' : question?.difficulty?.charAt(0).toUpperCase())}
|
|
799
|
+
</span>
|
|
800
|
+
</button>
|
|
801
|
+
|
|
802
|
+
{/* Folder actions menu - only for folders */}
|
|
803
|
+
{isFolder_ && folder && (
|
|
804
|
+
<DropdownMenu>
|
|
805
|
+
<DropdownMenuTrigger asChild>
|
|
806
|
+
<Button
|
|
807
|
+
type="button"
|
|
808
|
+
size="icon-xs"
|
|
809
|
+
variant="ghost"
|
|
810
|
+
aria-label={`Actions for folder ${folder.name}`}
|
|
811
|
+
className="shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
812
|
+
>
|
|
813
|
+
<i className="fa-light fa-ellipsis text-xs" aria-hidden="true" />
|
|
814
|
+
</Button>
|
|
815
|
+
</DropdownMenuTrigger>
|
|
816
|
+
<DropdownMenuContent align="end" className="w-40">
|
|
817
|
+
<DropdownMenuItem onSelect={() => onCustomizeFolder?.(folder)}>
|
|
818
|
+
<i className="fa-light fa-wand-magic-sparkles text-xs" aria-hidden="true" />
|
|
819
|
+
Customize
|
|
820
|
+
</DropdownMenuItem>
|
|
821
|
+
</DropdownMenuContent>
|
|
822
|
+
</DropdownMenu>
|
|
823
|
+
)}
|
|
824
|
+
</div>
|
|
825
|
+
)
|
|
826
|
+
})}
|
|
827
|
+
</div>
|
|
828
|
+
</ResizablePanel>
|
|
829
|
+
</React.Fragment>
|
|
830
|
+
))}
|
|
831
|
+
|
|
832
|
+
{/* Details panel — question (summary) or folder (aggregates) */}
|
|
833
|
+
{(selectedQuestion || selectedFolderLeaf) && (
|
|
834
|
+
<>
|
|
835
|
+
<ResizableHandle withHandle className={LIST_PAGE_SPLIT_RESIZABLE_HANDLE_CLASS} />
|
|
836
|
+
<ResizablePanel id="col-detail" defaultSize={30} minSize={20} className={LIST_PAGE_SPLIT_MILLER_DETAIL_PANEL_CLASS}>
|
|
837
|
+
{selectedQuestion ? (
|
|
838
|
+
<>
|
|
839
|
+
<ListPageTreeColumnHeader title="Details" className="px-4" />
|
|
840
|
+
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-4">
|
|
841
|
+
{panelRenderDetail(selectedQuestion)}
|
|
842
|
+
</div>
|
|
843
|
+
</>
|
|
844
|
+
) : selectedFolderLeaf ? (
|
|
845
|
+
<div className="min-h-0 flex-1 overflow-hidden">
|
|
846
|
+
<FolderDetailsShell
|
|
847
|
+
folder={selectedFolderLeaf}
|
|
848
|
+
folders={folders}
|
|
849
|
+
questions={rows}
|
|
850
|
+
/>
|
|
851
|
+
</div>
|
|
852
|
+
) : null}
|
|
853
|
+
</ResizablePanel>
|
|
854
|
+
</>
|
|
855
|
+
)}
|
|
856
|
+
</ResizablePanelGroup>
|
|
857
|
+
)
|
|
858
|
+
}
|
|
859
|
+
|
|
406
860
|
export type QuestionBankTableHandle = OpenTablePropertiesHandle
|
|
407
861
|
|
|
408
862
|
export const QuestionBankTable = React.forwardRef<
|
|
409
863
|
QuestionBankTableHandle,
|
|
410
|
-
{
|
|
411
|
-
|
|
412
|
-
|
|
864
|
+
{
|
|
865
|
+
items: QuestionBankItem[]
|
|
866
|
+
/** When set, table / board / tree rows are limited to this nav scope (secondary sidebar). */
|
|
867
|
+
navState?: QuestionBankNavState
|
|
868
|
+
view?: DataListViewType
|
|
869
|
+
onViewChange?: (v: DataListViewType) => void
|
|
870
|
+
folders: QuestionBankFolder[]
|
|
871
|
+
onFoldersChange: React.Dispatch<React.SetStateAction<QuestionBankFolder[]>>
|
|
872
|
+
onItemsChange: React.Dispatch<React.SetStateAction<QuestionBankItem[]>>
|
|
873
|
+
}
|
|
874
|
+
>(function QuestionBankTable(
|
|
875
|
+
{ items, navState, view = "table", onViewChange, folders, onFoldersChange, onItemsChange },
|
|
876
|
+
ref,
|
|
877
|
+
) {
|
|
878
|
+
const tableSourceItems = React.useMemo(() => {
|
|
879
|
+
const nav = navState ?? { scope: "all" as const, folderId: null }
|
|
880
|
+
return filterQuestionBankItemsByNav(items, folders, nav)
|
|
881
|
+
}, [items, folders, navState])
|
|
882
|
+
|
|
883
|
+
const columns = React.useMemo(() => buildQuestionBankColumns(tableSourceItems), [tableSourceItems])
|
|
413
884
|
const filterFields = React.useMemo(() => columnsToFilterFields(columns), [columns])
|
|
414
885
|
const fieldDefinitionsForDrawer = React.useMemo(
|
|
415
886
|
() =>
|
|
@@ -429,6 +900,10 @@ export const QuestionBankTable = React.forwardRef<
|
|
|
429
900
|
setDisplayOptions(prev => ({ ...prev, ...patch }))
|
|
430
901
|
}, [])
|
|
431
902
|
|
|
903
|
+
const [newFolderOpen, setNewFolderOpen] = React.useState(false)
|
|
904
|
+
const [newFolderParentId, setNewFolderParentId] = React.useState<string | null>(null)
|
|
905
|
+
const [customizingFolder, setCustomizingFolder] = React.useState<QuestionBankFolder | null>(null)
|
|
906
|
+
|
|
432
907
|
const [conditionalRules, setConditionalRules] = React.useState<ConditionalRule[]>([])
|
|
433
908
|
const addConditionalRule = React.useCallback((rule: Omit<ConditionalRule, "id">) => {
|
|
434
909
|
setConditionalRules(prev => [...prev, { ...rule, id: `cr-${Date.now()}` }])
|
|
@@ -440,7 +915,44 @@ export const QuestionBankTable = React.forwardRef<
|
|
|
440
915
|
setConditionalRules(prev => prev.map(r => r.id === id ? { ...r, ...patch } : r))
|
|
441
916
|
}, [])
|
|
442
917
|
|
|
443
|
-
const tableState = useTableState(
|
|
918
|
+
const tableState = useTableState(tableSourceItems, columns, { key: "updatedAt", dir: "desc" })
|
|
919
|
+
|
|
920
|
+
const openNewFolderForColumn = React.useCallback((parentId: string | null) => {
|
|
921
|
+
setNewFolderParentId(parentId)
|
|
922
|
+
setCustomizingFolder(null)
|
|
923
|
+
setNewFolderOpen(true)
|
|
924
|
+
}, [])
|
|
925
|
+
|
|
926
|
+
const openCustomizeFolderSheet = React.useCallback((folder: QuestionBankFolder) => {
|
|
927
|
+
setCustomizingFolder(folder)
|
|
928
|
+
setNewFolderOpen(true)
|
|
929
|
+
}, [])
|
|
930
|
+
|
|
931
|
+
const addQuestionInColumn = React.useCallback(
|
|
932
|
+
(parentId: string | null) => {
|
|
933
|
+
const folderId = defaultFolderIdForColumnParent(parentId, folders)
|
|
934
|
+
if (!folderId) return
|
|
935
|
+
const today = new Date()
|
|
936
|
+
const y = today.getFullYear()
|
|
937
|
+
const m = String(today.getMonth() + 1).padStart(2, "0")
|
|
938
|
+
const d = String(today.getDate()).padStart(2, "0")
|
|
939
|
+
onItemsChange(prev => [
|
|
940
|
+
...prev,
|
|
941
|
+
{
|
|
942
|
+
id: newQuestionBankItemId(),
|
|
943
|
+
stem: "New question",
|
|
944
|
+
topic: "General",
|
|
945
|
+
type: "short_answer",
|
|
946
|
+
difficulty: "medium",
|
|
947
|
+
status: "draft",
|
|
948
|
+
author: "Demo user",
|
|
949
|
+
updatedAt: `${y}-${m}-${d}`,
|
|
950
|
+
folderId,
|
|
951
|
+
},
|
|
952
|
+
])
|
|
953
|
+
},
|
|
954
|
+
[folders, onItemsChange],
|
|
955
|
+
)
|
|
444
956
|
|
|
445
957
|
const renderFilterOptionValue = React.useCallback(
|
|
446
958
|
(fieldKey: string, value: string): React.ReactNode => {
|
|
@@ -476,8 +988,62 @@ export const QuestionBankTable = React.forwardRef<
|
|
|
476
988
|
? displayOptions.boardGroupByColumnKey
|
|
477
989
|
: "status"
|
|
478
990
|
|
|
991
|
+
const panelRenderDetail = (row: QuestionBankItem) => (
|
|
992
|
+
<div className="flex min-w-0 flex-col gap-4">
|
|
993
|
+
<div>
|
|
994
|
+
<h3 className="text-sm font-semibold text-foreground mb-2">Question</h3>
|
|
995
|
+
<p className="text-sm text-foreground">{row.stem}</p>
|
|
996
|
+
</div>
|
|
997
|
+
<div className="flex gap-3 flex-wrap">
|
|
998
|
+
<div className="flex flex-col gap-1">
|
|
999
|
+
<span className="text-xs font-medium text-muted-foreground">Type</span>
|
|
1000
|
+
<span className="text-sm text-foreground">{TYPE_LABEL[row.type]}</span>
|
|
1001
|
+
</div>
|
|
1002
|
+
<div className="flex flex-col gap-1">
|
|
1003
|
+
<span className="text-xs font-medium text-muted-foreground">Difficulty</span>
|
|
1004
|
+
<span className="text-sm text-foreground">{DIFFICULTY_LABEL[row.difficulty]}</span>
|
|
1005
|
+
</div>
|
|
1006
|
+
<div className="flex flex-col gap-1">
|
|
1007
|
+
<span className="text-xs font-medium text-muted-foreground">Status</span>
|
|
1008
|
+
<span className="text-sm text-foreground">
|
|
1009
|
+
{QUESTION_BANK_STATUS_LABEL[row.status]}
|
|
1010
|
+
</span>
|
|
1011
|
+
</div>
|
|
1012
|
+
</div>
|
|
1013
|
+
{row.topic && (
|
|
1014
|
+
<div>
|
|
1015
|
+
<span className="text-xs font-medium text-muted-foreground">Topic</span>
|
|
1016
|
+
<p className="text-sm text-foreground mt-1">{row.topic}</p>
|
|
1017
|
+
</div>
|
|
1018
|
+
)}
|
|
1019
|
+
{row.type === "multiple_choice" && row.options && row.options.length > 0 && (
|
|
1020
|
+
<div>
|
|
1021
|
+
<span className="text-xs font-medium text-muted-foreground block mb-2">Options</span>
|
|
1022
|
+
<div className="flex flex-col gap-2">
|
|
1023
|
+
{row.options.map((option, idx) => (
|
|
1024
|
+
<div key={idx} className="flex items-start gap-2 p-2 rounded bg-muted/30">
|
|
1025
|
+
<span className="text-xs font-medium text-muted-foreground mt-0.5 shrink-0">
|
|
1026
|
+
{String.fromCharCode(65 + idx)}.
|
|
1027
|
+
</span>
|
|
1028
|
+
<span className={cn(
|
|
1029
|
+
"text-sm",
|
|
1030
|
+
option.isCorrect ? "text-foreground font-medium" : "text-foreground/80"
|
|
1031
|
+
)}>
|
|
1032
|
+
{option.text}
|
|
1033
|
+
</span>
|
|
1034
|
+
{option.isCorrect && (
|
|
1035
|
+
<i className="fa-light fa-check text-emerald-600 text-sm ml-auto shrink-0" aria-hidden="true" />
|
|
1036
|
+
)}
|
|
1037
|
+
</div>
|
|
1038
|
+
))}
|
|
1039
|
+
</div>
|
|
1040
|
+
</div>
|
|
1041
|
+
)}
|
|
1042
|
+
</div>
|
|
1043
|
+
)
|
|
1044
|
+
|
|
479
1045
|
const drawerToolbarProps = {
|
|
480
|
-
totalRows:
|
|
1046
|
+
totalRows: tableSourceItems.length,
|
|
481
1047
|
filterFields,
|
|
482
1048
|
fieldDefinitions: fieldDefinitionsForDrawer,
|
|
483
1049
|
resolveColumnLabel,
|
|
@@ -495,7 +1061,7 @@ export const QuestionBankTable = React.forwardRef<
|
|
|
495
1061
|
}
|
|
496
1062
|
|
|
497
1063
|
const tableProps = {
|
|
498
|
-
data:
|
|
1064
|
+
data: tableSourceItems,
|
|
499
1065
|
columns,
|
|
500
1066
|
getRowId: (row: QuestionBankItem) => row.id,
|
|
501
1067
|
getRowSelectionLabel: (row: QuestionBankItem) => row.stem,
|
|
@@ -528,14 +1094,6 @@ export const QuestionBankTable = React.forwardRef<
|
|
|
528
1094
|
},
|
|
529
1095
|
}
|
|
530
1096
|
|
|
531
|
-
if (view === "table") {
|
|
532
|
-
return (
|
|
533
|
-
<div className="pb-6">
|
|
534
|
-
<DataTable<QuestionBankItem> {...tableProps} />
|
|
535
|
-
</div>
|
|
536
|
-
)
|
|
537
|
-
}
|
|
538
|
-
|
|
539
1097
|
const sharedToolbar = (
|
|
540
1098
|
<DataTableToolbar
|
|
541
1099
|
state={tableState}
|
|
@@ -547,6 +1105,14 @@ export const QuestionBankTable = React.forwardRef<
|
|
|
547
1105
|
/>
|
|
548
1106
|
)
|
|
549
1107
|
|
|
1108
|
+
if (view === "table") {
|
|
1109
|
+
return (
|
|
1110
|
+
<div className="pb-6">
|
|
1111
|
+
<DataTable<QuestionBankItem> {...tableProps} />
|
|
1112
|
+
</div>
|
|
1113
|
+
)
|
|
1114
|
+
}
|
|
1115
|
+
|
|
550
1116
|
if (view === "list") {
|
|
551
1117
|
return (
|
|
552
1118
|
<div className="flex min-h-0 flex-1 flex-col">
|
|
@@ -568,16 +1134,135 @@ export const QuestionBankTable = React.forwardRef<
|
|
|
568
1134
|
)
|
|
569
1135
|
}
|
|
570
1136
|
|
|
1137
|
+
if (view === "folder") {
|
|
1138
|
+
return (
|
|
1139
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
1140
|
+
{sharedToolbar}
|
|
1141
|
+
<QuestionBankOsFolderView
|
|
1142
|
+
folders={folders}
|
|
1143
|
+
onFoldersChange={onFoldersChange}
|
|
1144
|
+
questions={tableState.rows as QuestionBankItem[]}
|
|
1145
|
+
onQuestionsChange={onItemsChange}
|
|
1146
|
+
/>
|
|
1147
|
+
</div>
|
|
1148
|
+
)
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
if (view === "panel") {
|
|
1152
|
+
return (
|
|
1153
|
+
<>
|
|
1154
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
1155
|
+
{sharedToolbar}
|
|
1156
|
+
<ListPageSplitHubChrome aria-label="Question bank folder columns">
|
|
1157
|
+
<HubFolderColumnsPanel
|
|
1158
|
+
folders={folders}
|
|
1159
|
+
rows={tableState.rows as QuestionBankItem[]}
|
|
1160
|
+
panelRenderDetail={panelRenderDetail}
|
|
1161
|
+
onAddFolder={openNewFolderForColumn}
|
|
1162
|
+
onAddQuestion={addQuestionInColumn}
|
|
1163
|
+
onCustomizeFolder={openCustomizeFolderSheet}
|
|
1164
|
+
/>
|
|
1165
|
+
</ListPageSplitHubChrome>
|
|
1166
|
+
</div>
|
|
1167
|
+
<QuestionBankNewFolderSheet
|
|
1168
|
+
open={newFolderOpen}
|
|
1169
|
+
onOpenChange={setNewFolderOpen}
|
|
1170
|
+
parentFolderId={customizingFolder?.parentId ?? newFolderParentId}
|
|
1171
|
+
customizingFolder={customizingFolder}
|
|
1172
|
+
onCreated={(newFolder) => {
|
|
1173
|
+
if (customizingFolder) {
|
|
1174
|
+
// Update existing folder
|
|
1175
|
+
onFoldersChange(prev =>
|
|
1176
|
+
prev.map(f =>
|
|
1177
|
+
f.id === customizingFolder.id
|
|
1178
|
+
? {
|
|
1179
|
+
...f,
|
|
1180
|
+
name: newFolder.name,
|
|
1181
|
+
icon: newFolder.icon,
|
|
1182
|
+
colorKey: newFolder.colorKey,
|
|
1183
|
+
}
|
|
1184
|
+
: f,
|
|
1185
|
+
),
|
|
1186
|
+
)
|
|
1187
|
+
setCustomizingFolder(null)
|
|
1188
|
+
} else {
|
|
1189
|
+
// Create new folder
|
|
1190
|
+
onFoldersChange(prev => [
|
|
1191
|
+
...prev,
|
|
1192
|
+
{
|
|
1193
|
+
id: `fld-${Date.now()}`,
|
|
1194
|
+
name: newFolder.name,
|
|
1195
|
+
icon: newFolder.icon,
|
|
1196
|
+
colorKey: newFolder.colorKey,
|
|
1197
|
+
parentId: newFolder.parentId,
|
|
1198
|
+
},
|
|
1199
|
+
])
|
|
1200
|
+
}
|
|
1201
|
+
setNewFolderOpen(false)
|
|
1202
|
+
}}
|
|
1203
|
+
/>
|
|
1204
|
+
</>
|
|
1205
|
+
)
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
if (view === "tree-panel") {
|
|
1209
|
+
return (
|
|
1210
|
+
<>
|
|
1211
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
1212
|
+
{sharedToolbar}
|
|
1213
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
1214
|
+
<HubTreePanelView
|
|
1215
|
+
items={tableState.rows as QuestionBankItem[]}
|
|
1216
|
+
folders={folders}
|
|
1217
|
+
onItemsChange={onItemsChange}
|
|
1218
|
+
onFoldersChange={onFoldersChange}
|
|
1219
|
+
/>
|
|
1220
|
+
</div>
|
|
1221
|
+
</div>
|
|
1222
|
+
<QuestionBankNewFolderSheet
|
|
1223
|
+
open={newFolderOpen}
|
|
1224
|
+
onOpenChange={setNewFolderOpen}
|
|
1225
|
+
parentFolderId={customizingFolder?.parentId ?? newFolderParentId}
|
|
1226
|
+
customizingFolder={customizingFolder}
|
|
1227
|
+
onCreated={(newFolder) => {
|
|
1228
|
+
if (customizingFolder) {
|
|
1229
|
+
// Update existing folder
|
|
1230
|
+
onFoldersChange(prev =>
|
|
1231
|
+
prev.map(f =>
|
|
1232
|
+
f.id === customizingFolder.id
|
|
1233
|
+
? {
|
|
1234
|
+
...f,
|
|
1235
|
+
name: newFolder.name,
|
|
1236
|
+
icon: newFolder.icon,
|
|
1237
|
+
colorKey: newFolder.colorKey,
|
|
1238
|
+
}
|
|
1239
|
+
: f,
|
|
1240
|
+
),
|
|
1241
|
+
)
|
|
1242
|
+
setCustomizingFolder(null)
|
|
1243
|
+
} else {
|
|
1244
|
+
// Create new folder
|
|
1245
|
+
onFoldersChange(prev => [
|
|
1246
|
+
...prev,
|
|
1247
|
+
{
|
|
1248
|
+
id: `fld-${Date.now()}`,
|
|
1249
|
+
name: newFolder.name,
|
|
1250
|
+
icon: newFolder.icon,
|
|
1251
|
+
colorKey: newFolder.colorKey,
|
|
1252
|
+
parentId: newFolder.parentId,
|
|
1253
|
+
},
|
|
1254
|
+
])
|
|
1255
|
+
}
|
|
1256
|
+
setNewFolderOpen(false)
|
|
1257
|
+
}}
|
|
1258
|
+
/>
|
|
1259
|
+
</>
|
|
1260
|
+
)
|
|
1261
|
+
}
|
|
1262
|
+
|
|
571
1263
|
return (
|
|
572
1264
|
<div className="flex min-h-0 flex-1 flex-col">
|
|
573
|
-
|
|
574
|
-
state={tableState}
|
|
575
|
-
columns={columns}
|
|
576
|
-
searchable={displayOptions.showToolbarSearch}
|
|
577
|
-
searchAriaLabel="Search questions"
|
|
578
|
-
renderFilterOptionValue={renderFilterOptionValue}
|
|
579
|
-
toolbarSlot={s => <TablePropertiesDrawerButton {...drawerToolbarProps} state={s} />}
|
|
580
|
-
/>
|
|
1265
|
+
{sharedToolbar}
|
|
581
1266
|
<QuestionBankDashboardSimple rows={tableState.rows as QuestionBankItem[]} />
|
|
582
1267
|
</div>
|
|
583
1268
|
)
|