@exxatdesignux/ui 0.2.18 → 0.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/consumer-extras/AGENTS.md +76 -0
- package/consumer-extras/README.md +5 -1
- package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +14 -3
- package/consumer-extras/cursor-skills/exxat-consumer-app/SKILL.md +37 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +21 -6
- package/consumer-extras/cursor-skills/exxat-focused-workflow-page/SKILL.md +57 -0
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +4 -2
- package/consumer-extras/patterns/consumer-app-pattern.md +39 -0
- package/consumer-extras/patterns/consumer-upgrade-checklist.md +20 -0
- package/consumer-extras/patterns/data-views-pattern.md +40 -3
- package/consumer-extras/patterns/focused-workflow-page-pattern.md +84 -0
- package/consumer-extras/patterns/shell-surface-elevation-pattern.md +5 -3
- package/package.json +2 -1
- package/src/components/ui/button-group.tsx +81 -0
- package/src/components/ui/button.tsx +4 -4
- package/src/globals.css +7 -1858
- package/src/theme.css +10 -1126
- package/src/tokens/README.md +15 -0
- package/src/tokens/base.css +337 -0
- package/src/tokens/high-contrast.css +1195 -0
- package/src/tokens/layers.css +224 -0
- package/src/tokens/tailwind-bridge.css +118 -0
- package/src/tokens/themes.css +201 -0
- package/template/AGENTS.md +60 -22
- package/template/app/(app)/dashboard/loading.tsx +3 -15
- package/template/app/(app)/dashboard/page.tsx +2 -14
- package/template/app/(app)/data-list/layout.tsx +43 -0
- package/template/app/(app)/data-list/page.tsx +2 -2
- package/template/app/(app)/examples/focused-workflow/page.tsx +5 -0
- package/template/app/(app)/examples/page.tsx +1 -0
- package/template/app/(app)/loading.tsx +1 -18
- package/template/app/(app)/question-bank/find/page.tsx +2 -1
- package/template/app/(app)/question-bank/library/page.tsx +2 -1
- package/template/app/(app)/question-bank/list/page.tsx +2 -1
- package/template/app/(app)/question-bank/new/page.tsx +15 -23
- package/template/app/(app)/question-bank/page.tsx +2 -1
- package/template/app/(app)/settings/page.tsx +4 -5
- package/template/app/globals.css +7 -1964
- package/template/components/app-route-loading.tsx +14 -0
- package/template/components/app-sidebar.tsx +70 -55
- package/template/components/data-views/index.ts +37 -9
- package/template/components/data-views/list-page-calendar-view.tsx +593 -0
- package/template/components/data-views/list-page-connected-view-body.tsx +66 -0
- package/template/components/data-views/list-page-folder-columns-panel.tsx +345 -0
- package/template/components/data-views/list-page-split-hub-chrome.tsx +8 -0
- package/template/components/examples/focused-workflow-showcase.tsx +183 -0
- package/template/components/list-hub-board-view.tsx +68 -0
- package/template/components/list-hub-client.tsx +186 -0
- package/template/components/list-hub-list-view.tsx +36 -0
- package/template/components/list-hub-panel-activator.tsx +8 -0
- package/template/components/list-hub-secondary-nav.tsx +121 -0
- package/template/components/list-hub-table.tsx +336 -0
- package/template/components/new-question-composer.tsx +6 -24
- package/template/components/product-switcher.tsx +3 -2
- package/template/components/question-bank-client.tsx +4 -1
- package/template/components/question-bank-folder-columns-panel.tsx +104 -0
- package/template/components/question-bank-table.tsx +143 -485
- package/template/components/secondary-panel/nav-link-rows.tsx +83 -0
- package/template/components/secondary-panel.tsx +4 -44
- package/template/components/secondary-panels/list-hub-panel.tsx +39 -0
- package/template/components/secondary-panels/question-bank-panel.tsx +39 -0
- package/template/components/secondary-panels/registry.tsx +15 -0
- package/template/components/settings-appearance-card.tsx +3 -2
- package/template/components/settings-client.tsx +59 -15
- package/template/components/settings-form-row.tsx +9 -4
- package/template/components/table-properties/drawer-button.tsx +13 -0
- package/template/components/table-properties/drawer.tsx +65 -4
- package/template/components/templates/focused-workflow-layouts.tsx +448 -0
- package/template/components/templates/focused-workflow-page-template.tsx +69 -0
- package/template/components/templates/list-page.tsx +29 -5
- package/template/components/templates/nested-secondary-panel-shell.tsx +2 -1
- package/template/components/templates/page-loading-shell.tsx +262 -0
- package/template/components/ui/button-group.tsx +1 -0
- package/template/docs/consumer-app-pattern.md +39 -0
- package/template/docs/data-views-pattern.md +40 -3
- package/template/docs/drawer-vs-dialog-pattern.md +3 -1
- package/template/docs/focused-workflow-page-pattern.md +84 -0
- package/template/docs/shell-surface-elevation-pattern.md +5 -3
- package/template/lib/command-menu-search-data.ts +11 -27
- package/template/lib/data-list-display-options.ts +16 -2
- package/template/lib/data-list-view-registry.ts +104 -0
- package/template/lib/data-list-view-surface.ts +15 -1
- package/template/lib/data-list-view.ts +10 -1
- package/template/lib/data-view-dashboard-storage.ts +38 -35
- package/template/lib/hub-connected-view-renderers.ts +58 -0
- package/template/lib/list-hub-nav.ts +121 -0
- package/template/lib/list-hub-supported-views.ts +10 -0
- package/template/lib/list-page-table-properties.ts +3 -7
- package/template/lib/list-status-badges.ts +4 -97
- package/template/lib/mock/list-hub-directory.ts +27 -0
- package/template/lib/mock/list-hub-kpi.ts +27 -0
- package/template/lib/mock/navigation.tsx +1 -0
- package/template/lib/page-loading-variant.ts +40 -0
- package/template/lib/question-bank-supported-views.ts +13 -0
- package/template/lib/table-state-lifecycle.ts +2 -2
- package/template/app/(app)/data-list/[id]/page.tsx +0 -44
- package/template/app/(app)/data-list/new/page.tsx +0 -34
- package/template/components/compliance-board-view.tsx +0 -142
- package/template/components/compliance-client.tsx +0 -92
- package/template/components/compliance-list-view.tsx +0 -54
- package/template/components/compliance-page-header.tsx +0 -89
- package/template/components/compliance-table.tsx +0 -612
- package/template/components/data-view-dashboard-charts-compliance.tsx +0 -963
- package/template/components/data-view-dashboard-charts-team.tsx +0 -971
- package/template/components/data-view-dashboard-charts.tsx +0 -1503
- package/template/components/new-placement-back-btn.tsx +0 -28
- package/template/components/new-placement-form.tsx +0 -1068
- package/template/components/placement-board-card.tsx +0 -262
- package/template/components/placement-detail.tsx +0 -438
- package/template/components/placements-board-view.tsx +0 -404
- package/template/components/placements-client.tsx +0 -252
- package/template/components/placements-list-view.tsx +0 -171
- package/template/components/placements-page-header.tsx +0 -166
- package/template/components/placements-table-cells.test.tsx +0 -22
- package/template/components/placements-table-cells.tsx +0 -173
- package/template/components/placements-table-columns.tsx +0 -640
- package/template/components/placements-table.tsx +0 -1642
- package/template/components/rotations-empty-state.tsx +0 -50
- package/template/components/rotations-panel-activator.tsx +0 -8
- package/template/components/sites-all-client.tsx +0 -154
- package/template/components/sites-board-view.tsx +0 -67
- package/template/components/sites-list-view.tsx +0 -42
- package/template/components/sites-table.tsx +0 -382
- package/template/components/team-board-view.tsx +0 -122
- package/template/components/team-client.tsx +0 -100
- package/template/components/team-list-view.tsx +0 -59
- package/template/components/team-page-header.tsx +0 -92
- package/template/components/team-table.tsx +0 -693
- package/template/lib/data-view-dashboard-placements-layout.ts +0 -215
- package/template/lib/mock/compliance-kpi.ts +0 -61
- package/template/lib/mock/compliance.ts +0 -146
- package/template/lib/mock/placements-kpi.ts +0 -134
- package/template/lib/mock/placements.ts +0 -183
- package/template/lib/mock/sites-directory.ts +0 -16
- package/template/lib/mock/sites-kpi.ts +0 -25
- package/template/lib/mock/team-kpi.ts +0 -60
- package/template/lib/mock/team.ts +0 -118
- package/template/lib/placement-board-card-layout.ts +0 -79
- package/template/lib/placement-lifecycle.ts +0 -5
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Question bank — DataTable + TablePropertiesDrawer +
|
|
4
|
+
* Question bank — DataTable + TablePropertiesDrawer + connected views via `ListPageConnectedViewBody`.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import * as React from "react"
|
|
@@ -10,6 +10,7 @@ import { mailtoHref } from "@/lib/mailto"
|
|
|
10
10
|
import { DataTable, DataTableToolbar } from "@/components/data-table"
|
|
11
11
|
import type { DataListViewType } from "@/lib/data-list-view"
|
|
12
12
|
import type { OpenTablePropertiesHandle } from "@/lib/list-page-table-properties"
|
|
13
|
+
import { QUESTION_BANK_SUPPORTED_VIEWS } from "@/lib/question-bank-supported-views"
|
|
13
14
|
import type { ColumnDef } from "@/components/data-table/types"
|
|
14
15
|
import { useTableState } from "@/components/data-table/use-table-state"
|
|
15
16
|
import { TablePropertiesDrawerButton } from "@/components/table-properties"
|
|
@@ -23,23 +24,11 @@ import {
|
|
|
23
24
|
} from "@/components/ui/dropdown-menu"
|
|
24
25
|
import { Tip } from "@/components/ui/tip"
|
|
25
26
|
import { Skeleton } from "@/components/ui/skeleton"
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
ResizablePanel,
|
|
29
|
-
ResizablePanelGroup,
|
|
30
|
-
} from "@/components/ui/resizable"
|
|
31
|
-
import {
|
|
32
|
-
Tooltip,
|
|
33
|
-
TooltipContent,
|
|
34
|
-
TooltipTrigger,
|
|
35
|
-
} from "@/components/ui/tooltip"
|
|
27
|
+
import { ListPageConnectedViewBody } from "@/components/data-views/list-page-connected-view-body"
|
|
28
|
+
import { ListPageCalendarView } from "@/components/data-views/list-page-calendar-view"
|
|
36
29
|
import { ListPageSplitHubChrome } from "@/components/data-views/list-page-split-hub-chrome"
|
|
37
|
-
import {
|
|
38
|
-
|
|
39
|
-
LIST_PAGE_SPLIT_RESIZABLE_HANDLE_CLASS,
|
|
40
|
-
LIST_PAGE_SPLIT_MILLER_COLUMN_PANEL_CLASS,
|
|
41
|
-
} from "@/components/data-views/list-page-split-hub-tokens"
|
|
42
|
-
import { ListPageTreeColumnHeader } from "@/components/data-views/list-page-tree-column-header"
|
|
30
|
+
import { defineHubViewRenderers } from "@/lib/hub-connected-view-renderers"
|
|
31
|
+
import { QuestionBankFolderColumnsPanel } from "@/components/question-bank-folder-columns-panel"
|
|
43
32
|
import { QuestionBankBoardView, QUESTION_BANK_BOARD_GROUP_OPTIONS } from "@/components/question-bank-board-view"
|
|
44
33
|
import { QuestionBankListView } from "@/components/question-bank-list-view"
|
|
45
34
|
import {
|
|
@@ -60,7 +49,7 @@ import {
|
|
|
60
49
|
type QuestionBankItem,
|
|
61
50
|
type QuestionBankType,
|
|
62
51
|
} from "@/lib/mock/question-bank"
|
|
63
|
-
import {
|
|
52
|
+
import type { QuestionBankFolder } from "@/lib/mock/question-bank-folders"
|
|
64
53
|
import {
|
|
65
54
|
toggleQuestionBankItemFavorite,
|
|
66
55
|
applyQuestionBankHubDisplayFilters,
|
|
@@ -328,312 +317,6 @@ function buildQuestionBankColumns(
|
|
|
328
317
|
return cols
|
|
329
318
|
}
|
|
330
319
|
|
|
331
|
-
interface HubFolderColumnsPanelProps {
|
|
332
|
-
folders: QuestionBankFolder[]
|
|
333
|
-
rows: QuestionBankItem[]
|
|
334
|
-
panelRenderDetail: (row: QuestionBankItem) => React.ReactNode
|
|
335
|
-
onAddFolder: (parentId: string | null) => void
|
|
336
|
-
onAddQuestion: (parentId: string | null) => void
|
|
337
|
-
onCustomizeFolder?: (folder: QuestionBankFolder) => void
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
type HierarchyItem = QuestionBankFolder | QuestionBankItem
|
|
341
|
-
|
|
342
|
-
function isFolder(item: HierarchyItem): item is QuestionBankFolder {
|
|
343
|
-
return 'parentId' in item
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function isQuestion(item: HierarchyItem): item is QuestionBankItem {
|
|
347
|
-
return 'stem' in item
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/** **Panel view** — multi-column folder explorer + optional detail column (Finder-style). */
|
|
351
|
-
function HubFolderColumnsPanel({
|
|
352
|
-
folders,
|
|
353
|
-
rows,
|
|
354
|
-
panelRenderDetail,
|
|
355
|
-
onAddFolder,
|
|
356
|
-
onAddQuestion,
|
|
357
|
-
onCustomizeFolder,
|
|
358
|
-
}: HubFolderColumnsPanelProps) {
|
|
359
|
-
// Track the selected path through the hierarchy
|
|
360
|
-
// Initialize with first folder selected by default
|
|
361
|
-
const [selectedPath, setSelectedPath] = React.useState<HierarchyItem[]>(() => {
|
|
362
|
-
const rootFolders = folders
|
|
363
|
-
.filter(f => f.parentId === null)
|
|
364
|
-
.sort((a, b) => a.name.localeCompare(b.name))
|
|
365
|
-
if (rootFolders.length > 0) {
|
|
366
|
-
return [rootFolders[0]]
|
|
367
|
-
}
|
|
368
|
-
return []
|
|
369
|
-
})
|
|
370
|
-
|
|
371
|
-
// Track if this is the first render for auto-selection
|
|
372
|
-
const isFirstRenderRef = React.useRef(true)
|
|
373
|
-
|
|
374
|
-
// Get root items (top-level folders)
|
|
375
|
-
const rootFolders = React.useMemo(() => {
|
|
376
|
-
return folders
|
|
377
|
-
.filter(f => f.parentId === null)
|
|
378
|
-
.sort((a, b) => a.name.localeCompare(b.name))
|
|
379
|
-
}, [folders])
|
|
380
|
-
|
|
381
|
-
// Handle selection at any depth
|
|
382
|
-
const handleSelect = (item: HierarchyItem, depth: number) => {
|
|
383
|
-
setSelectedPath(prev => [...prev.slice(0, depth), item])
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Auto-select first item at each level (only on first render). Intentional
|
|
387
|
-
// empty deps: we want this to run exactly once on mount; depending on the
|
|
388
|
-
// referenced values (folders / rows / selectedPath) would re-run on every
|
|
389
|
-
// edit and keep re-seeding the selection, undoing the user's choice.
|
|
390
|
-
React.useEffect(() => {
|
|
391
|
-
if (isFirstRenderRef.current && selectedPath.length > 0) {
|
|
392
|
-
const lastItem = selectedPath[selectedPath.length - 1]
|
|
393
|
-
if (isFolder(lastItem)) {
|
|
394
|
-
const folder = lastItem as QuestionBankFolder
|
|
395
|
-
const subfolders = folders.filter(f => f.parentId === folder.id).sort((a, b) => a.name.localeCompare(b.name))
|
|
396
|
-
const questionsInFolder = rows.filter(r => r.folderId === folder.id)
|
|
397
|
-
const items: HierarchyItem[] = [...subfolders, ...questionsInFolder]
|
|
398
|
-
|
|
399
|
-
if (items.length > 0 && !selectedPath[selectedPath.length + 1]) {
|
|
400
|
-
setSelectedPath(prev => [...prev, items[0]])
|
|
401
|
-
isFirstRenderRef.current = false
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
406
|
-
}, [])
|
|
407
|
-
|
|
408
|
-
// Build columns dynamically based on selected path
|
|
409
|
-
const columns: Array<{ items: HierarchyItem[]; depth: number; parentId?: string | null }> = React.useMemo(() => {
|
|
410
|
-
const cols: Array<{ items: HierarchyItem[]; depth: number; parentId?: string | null }> = [
|
|
411
|
-
{ items: rootFolders, depth: 0, parentId: null },
|
|
412
|
-
]
|
|
413
|
-
|
|
414
|
-
// For each selected folder in the path, add a column with its children
|
|
415
|
-
for (let i = 0; i < selectedPath.length; i++) {
|
|
416
|
-
const item = selectedPath[i]
|
|
417
|
-
if (isFolder(item)) {
|
|
418
|
-
// Get subfolders
|
|
419
|
-
const subfolders = folders
|
|
420
|
-
.filter(f => f.parentId === item.id)
|
|
421
|
-
.sort((a, b) => a.name.localeCompare(b.name))
|
|
422
|
-
|
|
423
|
-
// Get questions in this folder
|
|
424
|
-
const questionsInFolder = rows.filter(r => r.folderId === item.id)
|
|
425
|
-
|
|
426
|
-
// Combine folders and questions
|
|
427
|
-
const items: HierarchyItem[] = [...subfolders, ...questionsInFolder]
|
|
428
|
-
|
|
429
|
-
if (items.length > 0) {
|
|
430
|
-
cols.push({ items, depth: i + 1, parentId: item.id })
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
return cols
|
|
436
|
-
}, [selectedPath, rootFolders, folders, rows])
|
|
437
|
-
|
|
438
|
-
const selectedLeaf = selectedPath.length > 0 ? selectedPath.at(-1)! : null
|
|
439
|
-
const selectedQuestion =
|
|
440
|
-
selectedLeaf && isQuestion(selectedLeaf) ? (selectedLeaf as QuestionBankItem) : null
|
|
441
|
-
const selectedFolderLeaf =
|
|
442
|
-
selectedLeaf && isFolder(selectedLeaf) ? (selectedLeaf as QuestionBankFolder) : null
|
|
443
|
-
|
|
444
|
-
return (
|
|
445
|
-
<ResizablePanelGroup
|
|
446
|
-
direction="horizontal"
|
|
447
|
-
className="flex h-full min-h-0 w-full flex-1 overflow-hidden"
|
|
448
|
-
>
|
|
449
|
-
{/* Render all columns with handles between them */}
|
|
450
|
-
{columns.map(({ items, depth, parentId }, columnIdx) => (
|
|
451
|
-
<React.Fragment key={`col-${depth}`}>
|
|
452
|
-
{columnIdx > 0 && (
|
|
453
|
-
<ResizableHandle withHandle className={LIST_PAGE_SPLIT_RESIZABLE_HANDLE_CLASS} />
|
|
454
|
-
)}
|
|
455
|
-
<ResizablePanel
|
|
456
|
-
id={`col-${depth}`}
|
|
457
|
-
defaultSize={columnIdx === 0 ? 35 : columnIdx === 1 ? 35 : 30}
|
|
458
|
-
minSize={15}
|
|
459
|
-
className={LIST_PAGE_SPLIT_MILLER_COLUMN_PANEL_CLASS}
|
|
460
|
-
>
|
|
461
|
-
<ListPageTreeColumnHeader
|
|
462
|
-
title={
|
|
463
|
-
depth === 0
|
|
464
|
-
? "Categories"
|
|
465
|
-
: selectedPath[depth - 1] && isFolder(selectedPath[depth - 1])
|
|
466
|
-
? (selectedPath[depth - 1] as QuestionBankFolder).name
|
|
467
|
-
: "Items"
|
|
468
|
-
}
|
|
469
|
-
trailing={
|
|
470
|
-
<>
|
|
471
|
-
<span className="shrink-0 text-xs font-medium text-muted-foreground tabular-nums">
|
|
472
|
-
{items.length}
|
|
473
|
-
</span>
|
|
474
|
-
{depth < columns.length - 1 && items.length > 0 ? (
|
|
475
|
-
<div className="flex shrink-0 items-center gap-0.5">
|
|
476
|
-
<Tooltip>
|
|
477
|
-
<TooltipTrigger asChild>
|
|
478
|
-
<Button
|
|
479
|
-
size="icon-sm"
|
|
480
|
-
variant="ghost"
|
|
481
|
-
onClick={() => onAddFolder(parentId ?? null)}
|
|
482
|
-
aria-label="Add folder"
|
|
483
|
-
>
|
|
484
|
-
<i className="fa-light fa-folder-plus text-xs" aria-hidden="true" />
|
|
485
|
-
</Button>
|
|
486
|
-
</TooltipTrigger>
|
|
487
|
-
<TooltipContent side="top" sideOffset={4}>
|
|
488
|
-
Add folder
|
|
489
|
-
</TooltipContent>
|
|
490
|
-
</Tooltip>
|
|
491
|
-
<Tooltip>
|
|
492
|
-
<TooltipTrigger asChild>
|
|
493
|
-
<Button
|
|
494
|
-
size="icon-sm"
|
|
495
|
-
variant="ghost"
|
|
496
|
-
onClick={() => onAddQuestion(parentId ?? null)}
|
|
497
|
-
aria-label="Add question"
|
|
498
|
-
>
|
|
499
|
-
<i className="fa-light fa-plus text-xs" aria-hidden="true" />
|
|
500
|
-
</Button>
|
|
501
|
-
</TooltipTrigger>
|
|
502
|
-
<TooltipContent side="top" sideOffset={4}>
|
|
503
|
-
Add question
|
|
504
|
-
</TooltipContent>
|
|
505
|
-
</Tooltip>
|
|
506
|
-
</div>
|
|
507
|
-
) : null}
|
|
508
|
-
</>
|
|
509
|
-
}
|
|
510
|
-
/>
|
|
511
|
-
|
|
512
|
-
{/* Scrollable Items List */}
|
|
513
|
-
<div className="min-h-0 flex-1 overflow-y-auto py-1">
|
|
514
|
-
{items.map(item => {
|
|
515
|
-
const isSelected = selectedPath[depth]?.id === item.id
|
|
516
|
-
const isFolder_ = isFolder(item)
|
|
517
|
-
const folder = isFolder_ ? item : null
|
|
518
|
-
const question = isQuestion(item) ? item : null
|
|
519
|
-
|
|
520
|
-
// Get count for folders
|
|
521
|
-
const subfolderCount = isFolder_
|
|
522
|
-
? folders.filter(f => f.parentId === item.id).length
|
|
523
|
-
: 0
|
|
524
|
-
const questionCount = isFolder_
|
|
525
|
-
? rows.filter(r => r.folderId === item.id).length
|
|
526
|
-
: 0
|
|
527
|
-
const itemCount = subfolderCount + questionCount
|
|
528
|
-
|
|
529
|
-
return (
|
|
530
|
-
<div
|
|
531
|
-
key={item.id}
|
|
532
|
-
className="group flex items-center hover:bg-muted/50"
|
|
533
|
-
>
|
|
534
|
-
<button
|
|
535
|
-
onClick={() => handleSelect(item, depth)}
|
|
536
|
-
className={cn(
|
|
537
|
-
"flex flex-1 items-center gap-3 px-3 py-2 text-left text-sm transition-colors duration-75",
|
|
538
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
|
|
539
|
-
isSelected
|
|
540
|
-
? "bg-accent text-accent-foreground"
|
|
541
|
-
: "text-foreground",
|
|
542
|
-
// Apply folder background color if it's a folder and not selected, but NOT in the first column
|
|
543
|
-
isFolder_ && !isSelected && folder?.colorKey && depth > 0
|
|
544
|
-
? QUESTION_BANK_FOLDER_COLOR_STYLES[folder.colorKey]?.tile
|
|
545
|
-
: "",
|
|
546
|
-
)}
|
|
547
|
-
aria-selected={isSelected}
|
|
548
|
-
role="option"
|
|
549
|
-
>
|
|
550
|
-
{/* Icon - show for folders and questions */}
|
|
551
|
-
{isFolder_ ? (
|
|
552
|
-
<i className={cn(
|
|
553
|
-
"fa-folder text-sm shrink-0",
|
|
554
|
-
isSelected ? "fa-solid" : "fa-light",
|
|
555
|
-
// Apply folder color from customization (for both selected and unselected)
|
|
556
|
-
folder?.colorKey && QUESTION_BANK_FOLDER_ICON_COLORS[folder.colorKey]
|
|
557
|
-
)} aria-hidden="true" />
|
|
558
|
-
) : (
|
|
559
|
-
<i className={cn("fa-file text-sm shrink-0", isSelected ? "fa-solid" : "fa-light")} aria-hidden="true" />
|
|
560
|
-
)}
|
|
561
|
-
|
|
562
|
-
{/* Name */}
|
|
563
|
-
<span className={cn(
|
|
564
|
-
"min-w-0 flex-1 truncate leading-tight",
|
|
565
|
-
isSelected && "font-medium"
|
|
566
|
-
)}>
|
|
567
|
-
{isFolder_ ? folder?.name : question?.stem}
|
|
568
|
-
</span>
|
|
569
|
-
|
|
570
|
-
{/* Count or metadata */}
|
|
571
|
-
<span className={cn(
|
|
572
|
-
"shrink-0 tabular-nums text-xs ml-auto",
|
|
573
|
-
isSelected ? "text-accent-foreground/70" : "text-muted-foreground",
|
|
574
|
-
)}>
|
|
575
|
-
{isFolder_ ? itemCount : (question?.type === 'multiple_choice' ? 'MCQ' : question?.difficulty?.charAt(0).toUpperCase())}
|
|
576
|
-
</span>
|
|
577
|
-
</button>
|
|
578
|
-
|
|
579
|
-
{/* Folder actions menu - only for folders */}
|
|
580
|
-
{isFolder_ && folder && (
|
|
581
|
-
<DropdownMenu>
|
|
582
|
-
<DropdownMenuTrigger asChild>
|
|
583
|
-
<Button
|
|
584
|
-
type="button"
|
|
585
|
-
size="icon-xs"
|
|
586
|
-
variant="ghost"
|
|
587
|
-
aria-label={`Actions for folder ${folder.name}`}
|
|
588
|
-
className="shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
589
|
-
>
|
|
590
|
-
<i className="fa-light fa-ellipsis text-xs" aria-hidden="true" />
|
|
591
|
-
</Button>
|
|
592
|
-
</DropdownMenuTrigger>
|
|
593
|
-
<DropdownMenuContent align="end">
|
|
594
|
-
<DropdownMenuItem onSelect={() => onCustomizeFolder?.(folder)}>
|
|
595
|
-
<i className="fa-light fa-wand-magic-sparkles text-xs" aria-hidden="true" />
|
|
596
|
-
Customize
|
|
597
|
-
</DropdownMenuItem>
|
|
598
|
-
</DropdownMenuContent>
|
|
599
|
-
</DropdownMenu>
|
|
600
|
-
)}
|
|
601
|
-
</div>
|
|
602
|
-
)
|
|
603
|
-
})}
|
|
604
|
-
</div>
|
|
605
|
-
</ResizablePanel>
|
|
606
|
-
</React.Fragment>
|
|
607
|
-
))}
|
|
608
|
-
|
|
609
|
-
{/* Details panel — question (summary) or folder (aggregates) */}
|
|
610
|
-
{(selectedQuestion || selectedFolderLeaf) && (
|
|
611
|
-
<>
|
|
612
|
-
<ResizableHandle withHandle className={LIST_PAGE_SPLIT_RESIZABLE_HANDLE_CLASS} />
|
|
613
|
-
<ResizablePanel id="col-detail" defaultSize={30} minSize={20} className={LIST_PAGE_SPLIT_MILLER_DETAIL_PANEL_CLASS}>
|
|
614
|
-
{selectedQuestion ? (
|
|
615
|
-
<>
|
|
616
|
-
<ListPageTreeColumnHeader title="Details" className="px-4" />
|
|
617
|
-
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-4">
|
|
618
|
-
{panelRenderDetail(selectedQuestion)}
|
|
619
|
-
</div>
|
|
620
|
-
</>
|
|
621
|
-
) : selectedFolderLeaf ? (
|
|
622
|
-
<div className="min-h-0 flex-1 overflow-hidden">
|
|
623
|
-
<FolderDetailsShell
|
|
624
|
-
folder={selectedFolderLeaf}
|
|
625
|
-
folders={folders}
|
|
626
|
-
questions={rows}
|
|
627
|
-
/>
|
|
628
|
-
</div>
|
|
629
|
-
) : null}
|
|
630
|
-
</ResizablePanel>
|
|
631
|
-
</>
|
|
632
|
-
)}
|
|
633
|
-
</ResizablePanelGroup>
|
|
634
|
-
)
|
|
635
|
-
}
|
|
636
|
-
|
|
637
320
|
export type QuestionBankTableHandle = OpenTablePropertiesHandle
|
|
638
321
|
|
|
639
322
|
export const QuestionBankTable = React.forwardRef<
|
|
@@ -650,6 +333,8 @@ export const QuestionBankTable = React.forwardRef<
|
|
|
650
333
|
landingFilters?: QuestionBankLandingFilterState | null
|
|
651
334
|
view?: DataListViewType
|
|
652
335
|
onViewChange?: (v: DataListViewType) => void
|
|
336
|
+
/** Aligns Properties view tiles with `ListPageTemplate` `supportedViewTypes`. */
|
|
337
|
+
supportedViewTypes?: readonly DataListViewType[]
|
|
653
338
|
folders: QuestionBankFolder[]
|
|
654
339
|
onFoldersChange: React.Dispatch<React.SetStateAction<QuestionBankFolder[]>>
|
|
655
340
|
onItemsChange: React.Dispatch<React.SetStateAction<QuestionBankItem[]>>
|
|
@@ -663,6 +348,7 @@ export const QuestionBankTable = React.forwardRef<
|
|
|
663
348
|
landingFilters,
|
|
664
349
|
view = "table",
|
|
665
350
|
onViewChange,
|
|
351
|
+
supportedViewTypes = QUESTION_BANK_SUPPORTED_VIEWS,
|
|
666
352
|
folders,
|
|
667
353
|
onFoldersChange,
|
|
668
354
|
onItemsChange,
|
|
@@ -849,6 +535,7 @@ export const QuestionBankTable = React.forwardRef<
|
|
|
849
535
|
onUpdateConditionalRule: updateConditionalRule,
|
|
850
536
|
currentView: view,
|
|
851
537
|
onViewChange,
|
|
538
|
+
supportedViewTypes,
|
|
852
539
|
lifecycleTabLabel: "Question bank",
|
|
853
540
|
boardGroupByColumnOptions: [...QUESTION_BANK_BOARD_GROUP_OPTIONS],
|
|
854
541
|
renderFilterOptionValue,
|
|
@@ -899,172 +586,143 @@ export const QuestionBankTable = React.forwardRef<
|
|
|
899
586
|
/>
|
|
900
587
|
)
|
|
901
588
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
if (view === "list") {
|
|
911
|
-
return (
|
|
912
|
-
<div className="flex min-h-0 flex-1 flex-col">
|
|
913
|
-
{sharedToolbar}
|
|
914
|
-
<QuestionBankListView
|
|
915
|
-
rows={tableState.rows as QuestionBankItem[]}
|
|
916
|
-
onToggleFavorite={toggleFavorite}
|
|
917
|
-
onRowActivate={row => tableState.toggleRow(row.id)}
|
|
918
|
-
/>
|
|
919
|
-
</div>
|
|
920
|
-
)
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
if (view === "board") {
|
|
924
|
-
return (
|
|
925
|
-
<div className="flex min-h-0 flex-1 flex-col">
|
|
926
|
-
{sharedToolbar}
|
|
927
|
-
<QuestionBankBoardView
|
|
928
|
-
rows={tableState.rows as QuestionBankItem[]}
|
|
929
|
-
groupByColumnKey={questionBankBoardGroupKey}
|
|
930
|
-
onToggleFavorite={toggleFavorite}
|
|
931
|
-
onRowActivate={row => tableState.toggleRow(row.id)}
|
|
932
|
-
/>
|
|
933
|
-
</div>
|
|
934
|
-
)
|
|
935
|
-
}
|
|
589
|
+
const toolbarShell = (body: React.ReactNode) => (
|
|
590
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
591
|
+
{sharedToolbar}
|
|
592
|
+
{body}
|
|
593
|
+
</div>
|
|
594
|
+
)
|
|
936
595
|
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
596
|
+
const handleFolderSheetCreated = React.useCallback(
|
|
597
|
+
(newFolder: {
|
|
598
|
+
name: string
|
|
599
|
+
icon: string
|
|
600
|
+
colorKey: QuestionBankFolder["colorKey"]
|
|
601
|
+
parentId: string | null
|
|
602
|
+
}) => {
|
|
603
|
+
if (customizingFolder) {
|
|
604
|
+
onFoldersChange(prev =>
|
|
605
|
+
prev.map(f =>
|
|
606
|
+
f.id === customizingFolder.id
|
|
607
|
+
? { ...f, name: newFolder.name, icon: newFolder.icon, colorKey: newFolder.colorKey }
|
|
608
|
+
: f,
|
|
609
|
+
),
|
|
610
|
+
)
|
|
611
|
+
setCustomizingFolder(null)
|
|
612
|
+
} else {
|
|
613
|
+
onFoldersChange(prev => [
|
|
614
|
+
...prev,
|
|
615
|
+
{
|
|
616
|
+
id: `fld-${Date.now()}`,
|
|
617
|
+
name: newFolder.name,
|
|
618
|
+
icon: newFolder.icon,
|
|
619
|
+
colorKey: newFolder.colorKey,
|
|
620
|
+
parentId: newFolder.parentId,
|
|
621
|
+
},
|
|
622
|
+
])
|
|
623
|
+
}
|
|
624
|
+
setNewFolderOpen(false)
|
|
625
|
+
},
|
|
626
|
+
[customizingFolder, onFoldersChange],
|
|
627
|
+
)
|
|
950
628
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
panelRenderDetail={panelRenderDetail}
|
|
961
|
-
onAddFolder={openNewFolderForColumn}
|
|
962
|
-
onAddQuestion={addQuestionInColumn}
|
|
963
|
-
onCustomizeFolder={openCustomizeFolderSheet}
|
|
964
|
-
/>
|
|
965
|
-
</ListPageSplitHubChrome>
|
|
966
|
-
</div>
|
|
967
|
-
<QuestionBankNewFolderSheet
|
|
968
|
-
open={newFolderOpen}
|
|
969
|
-
onOpenChange={setNewFolderOpen}
|
|
970
|
-
parentFolderId={customizingFolder?.parentId ?? newFolderParentId}
|
|
971
|
-
customizingFolder={customizingFolder}
|
|
972
|
-
onCreated={(newFolder) => {
|
|
973
|
-
if (customizingFolder) {
|
|
974
|
-
// Update existing folder
|
|
975
|
-
onFoldersChange(prev =>
|
|
976
|
-
prev.map(f =>
|
|
977
|
-
f.id === customizingFolder.id
|
|
978
|
-
? {
|
|
979
|
-
...f,
|
|
980
|
-
name: newFolder.name,
|
|
981
|
-
icon: newFolder.icon,
|
|
982
|
-
colorKey: newFolder.colorKey,
|
|
983
|
-
}
|
|
984
|
-
: f,
|
|
985
|
-
),
|
|
986
|
-
)
|
|
987
|
-
setCustomizingFolder(null)
|
|
988
|
-
} else {
|
|
989
|
-
// Create new folder
|
|
990
|
-
onFoldersChange(prev => [
|
|
991
|
-
...prev,
|
|
992
|
-
{
|
|
993
|
-
id: `fld-${Date.now()}`,
|
|
994
|
-
name: newFolder.name,
|
|
995
|
-
icon: newFolder.icon,
|
|
996
|
-
colorKey: newFolder.colorKey,
|
|
997
|
-
parentId: newFolder.parentId,
|
|
998
|
-
},
|
|
999
|
-
])
|
|
1000
|
-
}
|
|
1001
|
-
setNewFolderOpen(false)
|
|
1002
|
-
}}
|
|
1003
|
-
/>
|
|
1004
|
-
</>
|
|
1005
|
-
)
|
|
1006
|
-
}
|
|
629
|
+
const folderSheet = (
|
|
630
|
+
<QuestionBankNewFolderSheet
|
|
631
|
+
open={newFolderOpen}
|
|
632
|
+
onOpenChange={setNewFolderOpen}
|
|
633
|
+
parentFolderId={customizingFolder?.parentId ?? newFolderParentId}
|
|
634
|
+
customizingFolder={customizingFolder}
|
|
635
|
+
onCreated={handleFolderSheetCreated}
|
|
636
|
+
/>
|
|
637
|
+
)
|
|
1007
638
|
|
|
1008
|
-
|
|
1009
|
-
return (
|
|
1010
|
-
<>
|
|
1011
|
-
<div className="flex min-h-0 flex-1 flex-col">
|
|
1012
|
-
{sharedToolbar}
|
|
1013
|
-
<div className="flex min-h-0 flex-1 flex-col">
|
|
1014
|
-
<HubTreePanelView
|
|
1015
|
-
items={tableState.rows as QuestionBankItem[]}
|
|
1016
|
-
folders={folders}
|
|
1017
|
-
onItemsChange={onItemsChange}
|
|
1018
|
-
onFoldersChange={onFoldersChange}
|
|
1019
|
-
/>
|
|
1020
|
-
</div>
|
|
1021
|
-
</div>
|
|
1022
|
-
<QuestionBankNewFolderSheet
|
|
1023
|
-
open={newFolderOpen}
|
|
1024
|
-
onOpenChange={setNewFolderOpen}
|
|
1025
|
-
parentFolderId={customizingFolder?.parentId ?? newFolderParentId}
|
|
1026
|
-
customizingFolder={customizingFolder}
|
|
1027
|
-
onCreated={(newFolder) => {
|
|
1028
|
-
if (customizingFolder) {
|
|
1029
|
-
// Update existing folder
|
|
1030
|
-
onFoldersChange(prev =>
|
|
1031
|
-
prev.map(f =>
|
|
1032
|
-
f.id === customizingFolder.id
|
|
1033
|
-
? {
|
|
1034
|
-
...f,
|
|
1035
|
-
name: newFolder.name,
|
|
1036
|
-
icon: newFolder.icon,
|
|
1037
|
-
colorKey: newFolder.colorKey,
|
|
1038
|
-
}
|
|
1039
|
-
: f,
|
|
1040
|
-
),
|
|
1041
|
-
)
|
|
1042
|
-
setCustomizingFolder(null)
|
|
1043
|
-
} else {
|
|
1044
|
-
// Create new folder
|
|
1045
|
-
onFoldersChange(prev => [
|
|
1046
|
-
...prev,
|
|
1047
|
-
{
|
|
1048
|
-
id: `fld-${Date.now()}`,
|
|
1049
|
-
name: newFolder.name,
|
|
1050
|
-
icon: newFolder.icon,
|
|
1051
|
-
colorKey: newFolder.colorKey,
|
|
1052
|
-
parentId: newFolder.parentId,
|
|
1053
|
-
},
|
|
1054
|
-
])
|
|
1055
|
-
}
|
|
1056
|
-
setNewFolderOpen(false)
|
|
1057
|
-
}}
|
|
1058
|
-
/>
|
|
1059
|
-
</>
|
|
1060
|
-
)
|
|
1061
|
-
}
|
|
639
|
+
const rows = tableState.rows as QuestionBankItem[]
|
|
1062
640
|
|
|
1063
641
|
return (
|
|
1064
|
-
<
|
|
1065
|
-
{
|
|
1066
|
-
|
|
1067
|
-
|
|
642
|
+
<ListPageConnectedViewBody
|
|
643
|
+
view={view}
|
|
644
|
+
hubLabel="Question bank"
|
|
645
|
+
renderers={defineHubViewRenderers(QUESTION_BANK_SUPPORTED_VIEWS, {
|
|
646
|
+
"data-table": (
|
|
647
|
+
<div className="pb-6">
|
|
648
|
+
<DataTable<QuestionBankItem> {...tableProps} />
|
|
649
|
+
</div>
|
|
650
|
+
),
|
|
651
|
+
"list-with-toolbar": toolbarShell(
|
|
652
|
+
<QuestionBankListView
|
|
653
|
+
rows={rows}
|
|
654
|
+
onToggleFavorite={toggleFavorite}
|
|
655
|
+
onRowActivate={row => tableState.toggleRow(row.id)}
|
|
656
|
+
/>,
|
|
657
|
+
),
|
|
658
|
+
"board-with-toolbar": toolbarShell(
|
|
659
|
+
<QuestionBankBoardView
|
|
660
|
+
rows={rows}
|
|
661
|
+
groupByColumnKey={questionBankBoardGroupKey}
|
|
662
|
+
onToggleFavorite={toggleFavorite}
|
|
663
|
+
onRowActivate={row => tableState.toggleRow(row.id)}
|
|
664
|
+
/>,
|
|
665
|
+
),
|
|
666
|
+
"folder-with-toolbar": toolbarShell(
|
|
667
|
+
<QuestionBankOsFolderView
|
|
668
|
+
folders={folders}
|
|
669
|
+
onFoldersChange={onFoldersChange}
|
|
670
|
+
questions={rows}
|
|
671
|
+
onQuestionsChange={onItemsChange}
|
|
672
|
+
/>,
|
|
673
|
+
),
|
|
674
|
+
"panel-with-toolbar": (
|
|
675
|
+
<>
|
|
676
|
+
{toolbarShell(
|
|
677
|
+
<ListPageSplitHubChrome aria-label="Question bank folder columns">
|
|
678
|
+
<QuestionBankFolderColumnsPanel
|
|
679
|
+
folders={folders}
|
|
680
|
+
rows={rows}
|
|
681
|
+
panelRenderDetail={panelRenderDetail}
|
|
682
|
+
onAddFolder={openNewFolderForColumn}
|
|
683
|
+
onAddQuestion={addQuestionInColumn}
|
|
684
|
+
onCustomizeFolder={openCustomizeFolderSheet}
|
|
685
|
+
/>
|
|
686
|
+
</ListPageSplitHubChrome>,
|
|
687
|
+
)}
|
|
688
|
+
{folderSheet}
|
|
689
|
+
</>
|
|
690
|
+
),
|
|
691
|
+
"tree-panel-with-toolbar": (
|
|
692
|
+
<>
|
|
693
|
+
{toolbarShell(
|
|
694
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
695
|
+
<HubTreePanelView
|
|
696
|
+
items={rows}
|
|
697
|
+
folders={folders}
|
|
698
|
+
onItemsChange={onItemsChange}
|
|
699
|
+
onFoldersChange={onFoldersChange}
|
|
700
|
+
/>
|
|
701
|
+
</div>,
|
|
702
|
+
)}
|
|
703
|
+
{folderSheet}
|
|
704
|
+
</>
|
|
705
|
+
),
|
|
706
|
+
"calendar-with-toolbar": toolbarShell(
|
|
707
|
+
<ListPageCalendarView
|
|
708
|
+
rows={rows}
|
|
709
|
+
getRowId={row => row.id}
|
|
710
|
+
getEventDate={row => row.updatedAt}
|
|
711
|
+
getEventLabel={row => row.stem}
|
|
712
|
+
getEventMeta={row => row.topic}
|
|
713
|
+
emptyMonthLabel="No questions on this day."
|
|
714
|
+
ariaLabel="Question bank calendar"
|
|
715
|
+
showSummaryPanel={displayOptions.showCalendarSummaryPanel}
|
|
716
|
+
calendarMainView={displayOptions.calendarMainView}
|
|
717
|
+
onCalendarMainViewChange={v => patchDisplay({ calendarMainView: v })}
|
|
718
|
+
onEventActivate={row => tableState.toggleRow(row.id)}
|
|
719
|
+
/>,
|
|
720
|
+
),
|
|
721
|
+
"dashboard-with-toolbar": toolbarShell(
|
|
722
|
+
<QuestionBankDashboardChartsSection rows={rows} />,
|
|
723
|
+
),
|
|
724
|
+
})}
|
|
725
|
+
/>
|
|
1068
726
|
)
|
|
1069
727
|
})
|
|
1070
728
|
|