@exxatdesignux/ui 0.2.14 → 0.2.16
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 +20 -0
- package/consumer-extras/cursor-skills/exxat-collaboration-access/SKILL.md +3 -1
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +3 -0
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +3 -1
- package/consumer-extras/patterns/collaboration-access-pattern.md +2 -0
- package/package.json +1 -1
- package/src/components/ui/dropdown-menu.tsx +2 -0
- package/src/components/ui/popover.tsx +2 -2
- package/src/components/ui/select.tsx +1 -1
- package/src/components/ui/tooltip.tsx +7 -1
- package/src/globals.css +27 -2
- package/src/theme.css +4 -2
- package/template/AGENTS.md +6 -4
- package/template/app/(app)/question-bank/layout.tsx +11 -4
- package/template/app/globals.css +34 -2
- package/template/components/app-sidebar.tsx +89 -41
- package/template/components/ask-leo-sidebar.tsx +1 -2
- package/template/components/compliance-board-view.tsx +11 -3
- package/template/components/compliance-list-view.tsx +16 -3
- package/template/components/compliance-table.tsx +5 -1
- package/template/components/data-table/index.tsx +25 -11
- package/template/components/data-views/finder-panel-view.tsx +2 -2
- package/template/components/data-views/index.ts +19 -0
- package/template/components/data-views/list-page-split-details-placeholder.tsx +3 -3
- package/template/components/data-views/list-page-split-hub-tokens.ts +1 -1
- package/template/components/data-views/list-page-tree-column-header.tsx +1 -1
- package/template/components/data-views/outline-tree-menu.tsx +157 -0
- package/template/components/data-views/question-bank-folder-tree-branch.tsx +210 -0
- package/template/components/exxat-product-logo.tsx +11 -72
- package/template/components/folder-details-shell.tsx +1 -1
- package/template/components/hub-tree-panel-view.tsx +88 -80
- package/template/components/key-metrics.tsx +50 -13
- package/template/components/page-header.tsx +19 -10
- package/template/components/product-switcher.tsx +1 -4
- package/template/components/question-bank-board-view.tsx +11 -2
- package/template/components/question-bank-client.tsx +111 -69
- package/template/components/question-bank-list-view.tsx +12 -1
- package/template/components/question-bank-page-header.tsx +18 -2
- package/template/components/question-bank-secondary-nav.tsx +12 -225
- package/template/components/question-bank-table.tsx +6 -1
- package/template/components/secondary-panel.tsx +1 -1
- package/template/components/site-header.tsx +21 -2
- package/template/components/team-board-view.tsx +11 -3
- package/template/components/team-list-view.tsx +16 -3
- package/template/components/team-table.tsx +6 -2
- package/template/components/templates/dedicated-search-landing-template.tsx +86 -20
- package/template/components/templates/list-page.tsx +1 -3
- package/template/components/templates/nested-secondary-panel-shell.tsx +3 -4
- package/template/docs/collaboration-access-pattern.md +2 -0
- package/template/docs/question-bank-hub-header-pattern.md +25 -0
- package/template/hooks/use-secondary-panel-hub-nav.ts +17 -1
- package/template/lib/mock/navigation.tsx +30 -1
- package/template/lib/question-bank-nav.ts +26 -0
- package/template/package.json +3 -3
- package/template/components/command-menu-01.tsx +0 -133
- package/template/components/command-menu-02.tsx +0 -386
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Question bank hub — ListPageTemplate + KeyMetrics + QuestionBankTable (Team / Compliance pattern).
|
|
5
5
|
* URL hash syncs the active view tab; `?scope=` + `folderId=` sync with the secondary nav (`lib/question-bank-nav.ts`).
|
|
6
|
+
* (Primary sidebar “Library” must not treat that hash as “off-route” — `app-sidebar` `isNavActive` ignores hash for `href` without `#…`.)
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import * as React from "react"
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
type DataListViewType,
|
|
15
16
|
} from "@/components/data-views"
|
|
16
17
|
import { QuestionBankPageHeader } from "@/components/question-bank-page-header"
|
|
18
|
+
import { QuestionBankNewFolderSheet } from "@/components/question-bank-new-folder-sheet"
|
|
17
19
|
import { CollaborationAccessFlow } from "@/components/collaboration-access-flow"
|
|
18
20
|
import { QuestionBankTable, type QuestionBankTableHandle } from "@/components/question-bank-table"
|
|
19
21
|
import { SecondaryPanelHubTemplate } from "@/components/templates/secondary-panel-hub-template"
|
|
@@ -22,7 +24,7 @@ import { KeyMetrics } from "@/components/key-metrics"
|
|
|
22
24
|
import { useSecondaryPanelHubNav } from "@/hooks/use-secondary-panel-hub-nav"
|
|
23
25
|
import { QUESTION_BANK_ITEMS } from "@/lib/mock/question-bank"
|
|
24
26
|
import { QUESTION_BANK_HEADER_COLLABORATORS } from "@/lib/mock/question-bank-header-collaborators"
|
|
25
|
-
import { DEFAULT_QUESTION_BANK_FOLDERS } from "@/lib/mock/question-bank-folders"
|
|
27
|
+
import { DEFAULT_QUESTION_BANK_FOLDERS, type QuestionBankFolder } from "@/lib/mock/question-bank-folders"
|
|
26
28
|
import { questionBankKpiInsight, questionBankKpiMetrics } from "@/lib/mock/question-bank-kpi"
|
|
27
29
|
import {
|
|
28
30
|
applyQuestionBankHubDisplayFilters,
|
|
@@ -97,6 +99,8 @@ export function QuestionBankClient() {
|
|
|
97
99
|
parseNav: parseQuestionBankNav,
|
|
98
100
|
canonicalHref: questionBankCanonicalNavHref,
|
|
99
101
|
shouldReopenPanel: isQuestionBankDefaultNav,
|
|
102
|
+
/** Hub/find + list are full-width — layout closes the panel; do not fight it with `openPanel`. */
|
|
103
|
+
reopenPanelOnPathnames: [QUESTION_BANK_LIBRARY_PATH],
|
|
100
104
|
})
|
|
101
105
|
const isDedicatedSearch = isQuestionBankDedicatedSearchPathname(pathname)
|
|
102
106
|
const isHubFindSurface = pathname === QUESTION_BANK_HUB_FIND_PATH
|
|
@@ -173,6 +177,17 @@ export function QuestionBankClient() {
|
|
|
173
177
|
const [items, setItems] = React.useState(() => QUESTION_BANK_ITEMS.map(q => ({ ...q })))
|
|
174
178
|
const [folders, setFolders] = React.useState(() => DEFAULT_QUESTION_BANK_FOLDERS.map(f => ({ ...f })))
|
|
175
179
|
|
|
180
|
+
const [hubFolderCustomizeSheetOpen, setHubFolderCustomizeSheetOpen] = React.useState(false)
|
|
181
|
+
const [hubFolderCustomizeTarget, setHubFolderCustomizeTarget] = React.useState<QuestionBankFolder | null>(null)
|
|
182
|
+
|
|
183
|
+
const openHubScopedFolderCustomize = React.useCallback(() => {
|
|
184
|
+
if (navState.scope !== "folder" || !navState.folderId) return
|
|
185
|
+
const f = folders.find(x => x.id === navState.folderId)
|
|
186
|
+
if (!f) return
|
|
187
|
+
setHubFolderCustomizeTarget(f)
|
|
188
|
+
setHubFolderCustomizeSheetOpen(true)
|
|
189
|
+
}, [folders, navState.folderId, navState.scope])
|
|
190
|
+
|
|
176
191
|
const filteredItems = React.useMemo(
|
|
177
192
|
() => applyQuestionBankHubDisplayFilters(items, folders, navState, landingFilters),
|
|
178
193
|
[items, folders, landingFilters, navState],
|
|
@@ -331,76 +346,103 @@ export function QuestionBankClient() {
|
|
|
331
346
|
resourceLabel={hubHeader.title}
|
|
332
347
|
>
|
|
333
348
|
{({ collaborators, openInvite }) => (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
siteHeader={{
|
|
347
|
-
title: hubHeader.title,
|
|
348
|
-
breadcrumbs: hubHeader.breadcrumbs,
|
|
349
|
-
}}
|
|
350
|
-
>
|
|
351
|
-
<ListPageTemplate
|
|
352
|
-
defaultTabs={DEFAULT_TABS}
|
|
353
|
-
tabs={tabs}
|
|
354
|
-
onTabsChange={setTabs}
|
|
355
|
-
activeTabId={activeTabId}
|
|
356
|
-
onActiveTabChange={onActiveTabChange}
|
|
357
|
-
getTabCount={() => count}
|
|
358
|
-
tablePropertiesRef={tableRef}
|
|
359
|
-
header={(
|
|
360
|
-
<QuestionBankPageHeader
|
|
361
|
-
variant="collaboration"
|
|
362
|
-
title={hubHeader.title}
|
|
363
|
-
questionCount={count}
|
|
364
|
-
collaborators={collaborators}
|
|
365
|
-
onNewQuestion={() => {}}
|
|
366
|
-
onExport={() => setExportOpen(true)}
|
|
367
|
-
onAddCollaborator={openInvite}
|
|
368
|
-
onCollaboratorsOpen={openInvite}
|
|
369
|
-
showMetrics={showMetrics}
|
|
370
|
-
onToggleMetrics={() => setShowMetrics(v => !v)}
|
|
371
|
-
/>
|
|
372
|
-
)}
|
|
373
|
-
metrics={(
|
|
374
|
-
<KeyMetrics
|
|
375
|
-
variant="flat"
|
|
376
|
-
metrics={metrics}
|
|
377
|
-
insight={insight}
|
|
378
|
-
showHeader={false}
|
|
379
|
-
metricsSingleRow
|
|
380
|
-
/>
|
|
381
|
-
)}
|
|
382
|
-
showMetrics={showMetrics}
|
|
383
|
-
exportOpen={exportOpen}
|
|
384
|
-
onExportOpenChange={setExportOpen}
|
|
385
|
-
exportTotalRows={count}
|
|
386
|
-
renderContent={(tab, updateTab) => (
|
|
387
|
-
<QuestionBankTable
|
|
388
|
-
key={tab.id}
|
|
389
|
-
ref={tableRef}
|
|
390
|
-
items={items}
|
|
391
|
-
navState={navState}
|
|
392
|
-
urlListSearch={urlToolbarSearchSync}
|
|
393
|
-
landingFilters={null}
|
|
394
|
-
searchLanding={false}
|
|
395
|
-
folders={folders}
|
|
396
|
-
onFoldersChange={setFolders}
|
|
397
|
-
onItemsChange={setItems}
|
|
398
|
-
view={tab.viewType}
|
|
399
|
-
onViewChange={(v: DataListViewType) => updateTab({ viewType: v, icon: dataListViewIcon(v) })}
|
|
400
|
-
/>
|
|
349
|
+
<>
|
|
350
|
+
<SecondaryPanelHubTemplate
|
|
351
|
+
bridges={(
|
|
352
|
+
<>
|
|
353
|
+
<QuestionBankFolderBridge
|
|
354
|
+
folders={folders}
|
|
355
|
+
onFoldersChange={setFolders}
|
|
356
|
+
items={items}
|
|
357
|
+
onItemsChange={setItems}
|
|
358
|
+
/>
|
|
359
|
+
<QuestionBankAccessBridge openManageAccess={openInvite} />
|
|
360
|
+
</>
|
|
401
361
|
)}
|
|
362
|
+
siteHeader={{
|
|
363
|
+
title: hubHeader.title,
|
|
364
|
+
breadcrumbs: hubHeader.breadcrumbs,
|
|
365
|
+
}}
|
|
366
|
+
>
|
|
367
|
+
<ListPageTemplate
|
|
368
|
+
defaultTabs={DEFAULT_TABS}
|
|
369
|
+
tabs={tabs}
|
|
370
|
+
onTabsChange={setTabs}
|
|
371
|
+
activeTabId={activeTabId}
|
|
372
|
+
onActiveTabChange={onActiveTabChange}
|
|
373
|
+
getTabCount={() => count}
|
|
374
|
+
tablePropertiesRef={tableRef}
|
|
375
|
+
header={(
|
|
376
|
+
<QuestionBankPageHeader
|
|
377
|
+
variant="collaboration"
|
|
378
|
+
title={hubHeader.title}
|
|
379
|
+
questionCount={count}
|
|
380
|
+
collaborators={collaborators}
|
|
381
|
+
onNewQuestion={() => {}}
|
|
382
|
+
onExport={() => setExportOpen(true)}
|
|
383
|
+
onAddCollaborator={openInvite}
|
|
384
|
+
onCollaboratorsOpen={openInvite}
|
|
385
|
+
showMetrics={showMetrics}
|
|
386
|
+
onToggleMetrics={() => setShowMetrics(v => !v)}
|
|
387
|
+
onCustomizeFolder={
|
|
388
|
+
navState.scope === "folder" && navState.folderId ? openHubScopedFolderCustomize : undefined
|
|
389
|
+
}
|
|
390
|
+
/>
|
|
391
|
+
)}
|
|
392
|
+
metrics={(
|
|
393
|
+
<KeyMetrics
|
|
394
|
+
variant="flat"
|
|
395
|
+
metrics={metrics}
|
|
396
|
+
insight={insight}
|
|
397
|
+
showHeader={false}
|
|
398
|
+
metricsSingleRow
|
|
399
|
+
/>
|
|
400
|
+
)}
|
|
401
|
+
showMetrics={showMetrics}
|
|
402
|
+
exportOpen={exportOpen}
|
|
403
|
+
onExportOpenChange={setExportOpen}
|
|
404
|
+
exportTotalRows={count}
|
|
405
|
+
renderContent={(tab, updateTab) => (
|
|
406
|
+
<QuestionBankTable
|
|
407
|
+
key={tab.id}
|
|
408
|
+
ref={tableRef}
|
|
409
|
+
items={items}
|
|
410
|
+
navState={navState}
|
|
411
|
+
urlListSearch={urlToolbarSearchSync}
|
|
412
|
+
landingFilters={null}
|
|
413
|
+
searchLanding={false}
|
|
414
|
+
folders={folders}
|
|
415
|
+
onFoldersChange={setFolders}
|
|
416
|
+
onItemsChange={setItems}
|
|
417
|
+
view={tab.viewType}
|
|
418
|
+
onViewChange={(v: DataListViewType) => updateTab({ viewType: v, icon: dataListViewIcon(v) })}
|
|
419
|
+
/>
|
|
420
|
+
)}
|
|
421
|
+
/>
|
|
422
|
+
</SecondaryPanelHubTemplate>
|
|
423
|
+
<QuestionBankNewFolderSheet
|
|
424
|
+
open={hubFolderCustomizeSheetOpen}
|
|
425
|
+
onOpenChange={open => {
|
|
426
|
+
setHubFolderCustomizeSheetOpen(open)
|
|
427
|
+
if (!open) setHubFolderCustomizeTarget(null)
|
|
428
|
+
}}
|
|
429
|
+
parentFolderId={hubFolderCustomizeTarget?.parentId ?? null}
|
|
430
|
+
customizingFolder={hubFolderCustomizeTarget}
|
|
431
|
+
descriptionText="Update how this folder appears in the bank. Name, color, and icon apply everywhere the folder is shown."
|
|
432
|
+
onCreated={newFolder => {
|
|
433
|
+
const target = hubFolderCustomizeTarget
|
|
434
|
+
if (!target) return
|
|
435
|
+
setFolders(prev =>
|
|
436
|
+
prev.map(f =>
|
|
437
|
+
f.id === target.id
|
|
438
|
+
? { ...f, name: newFolder.name, icon: newFolder.icon, colorKey: newFolder.colorKey }
|
|
439
|
+
: f,
|
|
440
|
+
),
|
|
441
|
+
)
|
|
442
|
+
setHubFolderCustomizeTarget(null)
|
|
443
|
+
}}
|
|
402
444
|
/>
|
|
403
|
-
|
|
445
|
+
</>
|
|
404
446
|
)}
|
|
405
447
|
</CollaborationAccessFlow>
|
|
406
448
|
)
|
|
@@ -11,9 +11,11 @@ import {
|
|
|
11
11
|
function QuestionBankListRow({
|
|
12
12
|
row,
|
|
13
13
|
onToggleFavorite,
|
|
14
|
+
onRowActivate,
|
|
14
15
|
}: {
|
|
15
16
|
row: QuestionBankItem
|
|
16
17
|
onToggleFavorite: (row: QuestionBankItem) => void
|
|
18
|
+
onRowActivate?: (row: QuestionBankItem) => void
|
|
17
19
|
}) {
|
|
18
20
|
return (
|
|
19
21
|
<li>
|
|
@@ -21,6 +23,7 @@ function QuestionBankListRow({
|
|
|
21
23
|
className={QUESTION_BANK_FAVORITE_HOVER_GROUP}
|
|
22
24
|
layout="row"
|
|
23
25
|
rowContainerClassName="flex w-full flex-col gap-1 sm:flex-row sm:items-center sm:gap-4"
|
|
26
|
+
onClick={onRowActivate ? () => onRowActivate(row) : undefined}
|
|
24
27
|
rowEnd={(
|
|
25
28
|
<div className="flex shrink-0 items-center gap-1">
|
|
26
29
|
<QuestionBankFavoriteButton row={row} onToggleFavorite={onToggleFavorite} />
|
|
@@ -44,9 +47,12 @@ function QuestionBankListRow({
|
|
|
44
47
|
export function QuestionBankListView({
|
|
45
48
|
rows,
|
|
46
49
|
onToggleFavorite,
|
|
50
|
+
onRowActivate,
|
|
47
51
|
}: {
|
|
48
52
|
rows: QuestionBankItem[]
|
|
49
53
|
onToggleFavorite: (row: QuestionBankItem) => void
|
|
54
|
+
/** When set (e.g. table selection), clicking a row toggles the same selection as the grid. */
|
|
55
|
+
onRowActivate?: (row: QuestionBankItem) => void
|
|
50
56
|
}) {
|
|
51
57
|
if (rows.length === 0) {
|
|
52
58
|
return (
|
|
@@ -59,7 +65,12 @@ export function QuestionBankListView({
|
|
|
59
65
|
return (
|
|
60
66
|
<ul className="flex list-none flex-col gap-2 px-4 pb-8 pt-2 lg:px-6">
|
|
61
67
|
{rows.map(row => (
|
|
62
|
-
<QuestionBankListRow
|
|
68
|
+
<QuestionBankListRow
|
|
69
|
+
key={row.id}
|
|
70
|
+
row={row}
|
|
71
|
+
onToggleFavorite={onToggleFavorite}
|
|
72
|
+
onRowActivate={onRowActivate}
|
|
73
|
+
/>
|
|
63
74
|
))}
|
|
64
75
|
</ul>
|
|
65
76
|
)
|
|
@@ -27,7 +27,7 @@ export interface QuestionBankPageHeaderProps {
|
|
|
27
27
|
showMetrics?: boolean
|
|
28
28
|
onToggleMetrics?: () => void
|
|
29
29
|
showTitleBlock?: boolean
|
|
30
|
-
/** `collaboration` adds access line + collaborator
|
|
30
|
+
/** `collaboration` adds access line + collaborator face row before CTAs. */
|
|
31
31
|
variant?: PageHeaderVariant
|
|
32
32
|
/** Optional role / access row when `variant="collaboration"` (badge + copy). */
|
|
33
33
|
accessInfo?: React.ReactNode
|
|
@@ -44,6 +44,11 @@ export interface QuestionBankPageHeaderProps {
|
|
|
44
44
|
subtitleOverride?: string
|
|
45
45
|
/** Omits the action column (e.g. search landing before first query). */
|
|
46
46
|
hideActions?: boolean
|
|
47
|
+
/**
|
|
48
|
+
* When provided, the **More** menu includes **Customize folder** (opens the hub folder sheet).
|
|
49
|
+
* Wire this when the library is scoped to a folder (`?scope=folder&folderId=…`).
|
|
50
|
+
*/
|
|
51
|
+
onCustomizeFolder?: () => void
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
export function QuestionBankPageHeader({
|
|
@@ -57,7 +62,7 @@ export function QuestionBankPageHeader({
|
|
|
57
62
|
variant = "default",
|
|
58
63
|
accessInfo,
|
|
59
64
|
collaborators = QUESTION_BANK_HEADER_COLLABORATORS,
|
|
60
|
-
collaboratorDisplayLimit =
|
|
65
|
+
collaboratorDisplayLimit = 3,
|
|
61
66
|
onAddCollaborator = () => {},
|
|
62
67
|
onCollaboratorsOpen,
|
|
63
68
|
collaborationAddLabel = COLLABORATION_HEADER_ADD_LABEL,
|
|
@@ -65,6 +70,7 @@ export function QuestionBankPageHeader({
|
|
|
65
70
|
hideNewQuestion = false,
|
|
66
71
|
subtitleOverride,
|
|
67
72
|
hideActions = false,
|
|
73
|
+
onCustomizeFolder,
|
|
68
74
|
}: QuestionBankPageHeaderProps) {
|
|
69
75
|
const [moreOpen, setMoreOpen] = React.useState(false)
|
|
70
76
|
const countLine =
|
|
@@ -119,6 +125,16 @@ export function QuestionBankPageHeader({
|
|
|
119
125
|
{addCollaboratorLabel}
|
|
120
126
|
</DropdownMenuItem>
|
|
121
127
|
) : null}
|
|
128
|
+
{onCustomizeFolder ? (
|
|
129
|
+
<DropdownMenuItem
|
|
130
|
+
onSelect={() => {
|
|
131
|
+
window.setTimeout(() => onCustomizeFolder(), 0)
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
<i className="fa-light fa-wand-magic-sparkles" aria-hidden="true" />
|
|
135
|
+
Customize folder
|
|
136
|
+
</DropdownMenuItem>
|
|
137
|
+
) : null}
|
|
122
138
|
<DropdownMenuItem
|
|
123
139
|
onSelect={() => {
|
|
124
140
|
window.setTimeout(() => onExport(), 0)
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
import * as React from "react"
|
|
9
9
|
import Link from "next/link"
|
|
10
10
|
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
|
11
|
-
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
|
|
12
11
|
import { Button } from "@/components/ui/button"
|
|
13
12
|
import {
|
|
14
13
|
Dialog,
|
|
@@ -30,43 +29,19 @@ import { Kbd, KbdGroup } from "@/components/ui/kbd"
|
|
|
30
29
|
import { Tip } from "@/components/ui/tip"
|
|
31
30
|
import { cn } from "@/lib/utils"
|
|
32
31
|
import type { QuestionBankFolder } from "@/lib/mock/question-bank-folders"
|
|
33
|
-
import { QUESTION_BANK_FOLDER_ICON_COLORS } from "@/lib/mock/question-bank-folders"
|
|
34
32
|
import { DEFAULT_QUESTION_BANK_FOLDERS, newFolderId, collectFolderDescendantIds } from "@/lib/mock/question-bank-folders"
|
|
35
33
|
import { useSecondaryPanel } from "@/components/secondary-panel"
|
|
36
34
|
import { QuestionBankNewFolderSheet } from "@/components/question-bank-new-folder-sheet"
|
|
37
35
|
import {
|
|
38
|
-
|
|
36
|
+
isQuestionBankNavActive,
|
|
39
37
|
parseQuestionBankNav,
|
|
40
38
|
QUESTION_BANK_FAVORITES_FOLDER_ID,
|
|
41
39
|
QUESTION_BANK_LIBRARY_HUB_PATHS,
|
|
42
|
-
isQuestionBankSearchNavActive,
|
|
43
40
|
questionBankFavoritesFolderHref,
|
|
44
41
|
questionBankHubScopeHref,
|
|
45
|
-
questionBankSearchLandingNavHref,
|
|
46
42
|
type QuestionBankNavScope,
|
|
47
43
|
} from "@/lib/question-bank-nav"
|
|
48
|
-
|
|
49
|
-
function isNavActive(
|
|
50
|
-
pathname: string,
|
|
51
|
-
nav: ReturnType<typeof parseQuestionBankNav>,
|
|
52
|
-
scope: QuestionBankNavScope,
|
|
53
|
-
folderId?: string | null,
|
|
54
|
-
) {
|
|
55
|
-
if (!QUESTION_BANK_LIBRARY_HUB_PATHS.includes(pathname)) return false
|
|
56
|
-
// Dedicated search shells (list / hub-find) use the “Search” row for All/My — not these rows.
|
|
57
|
-
if (scope === "all") {
|
|
58
|
-
if (isQuestionBankDedicatedSearchPathname(pathname)) return false
|
|
59
|
-
return nav.scope === "all"
|
|
60
|
-
}
|
|
61
|
-
if (scope === "my") {
|
|
62
|
-
if (isQuestionBankDedicatedSearchPathname(pathname)) return false
|
|
63
|
-
return nav.scope === "my"
|
|
64
|
-
}
|
|
65
|
-
if (scope === "folder" && folderId) {
|
|
66
|
-
return nav.scope === "folder" && nav.folderId === folderId
|
|
67
|
-
}
|
|
68
|
-
return false
|
|
69
|
-
}
|
|
44
|
+
import { QuestionBankFolderTreeBranch } from "@/components/data-views/question-bank-folder-tree-branch"
|
|
70
45
|
|
|
71
46
|
function NavRow({
|
|
72
47
|
href,
|
|
@@ -147,179 +122,6 @@ function IconNavRow({
|
|
|
147
122
|
)
|
|
148
123
|
}
|
|
149
124
|
|
|
150
|
-
function PanelFolderBranch({
|
|
151
|
-
folder,
|
|
152
|
-
folders,
|
|
153
|
-
depth,
|
|
154
|
-
pathname,
|
|
155
|
-
hubSearchParams,
|
|
156
|
-
nav,
|
|
157
|
-
canManageFolders,
|
|
158
|
-
canManageAccess,
|
|
159
|
-
onAddSubfolder,
|
|
160
|
-
onCustomizeFolder,
|
|
161
|
-
onManageAccess,
|
|
162
|
-
onDeleteFolder,
|
|
163
|
-
}: {
|
|
164
|
-
folder: QuestionBankFolder
|
|
165
|
-
folders: QuestionBankFolder[]
|
|
166
|
-
depth: number
|
|
167
|
-
pathname: string
|
|
168
|
-
hubSearchParams: URLSearchParams
|
|
169
|
-
nav: ReturnType<typeof parseQuestionBankNav>
|
|
170
|
-
canManageFolders: boolean
|
|
171
|
-
canManageAccess: boolean
|
|
172
|
-
onAddSubfolder: (parentId: string) => void
|
|
173
|
-
onCustomizeFolder: (folder: QuestionBankFolder) => void
|
|
174
|
-
onManageAccess: () => void
|
|
175
|
-
onDeleteFolder: (folder: QuestionBankFolder) => void
|
|
176
|
-
}) {
|
|
177
|
-
const childFolders = React.useMemo(
|
|
178
|
-
() =>
|
|
179
|
-
folders
|
|
180
|
-
.filter(f => f.parentId === folder.id)
|
|
181
|
-
.sort((a, b) => a.name.localeCompare(b.name)),
|
|
182
|
-
[folders, folder.id],
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
const hasSubfolders = childFolders.length > 0
|
|
186
|
-
const indent = depth * 10
|
|
187
|
-
|
|
188
|
-
const folderHref = questionBankHubScopeHref(pathname, hubSearchParams, {
|
|
189
|
-
scope: "folder",
|
|
190
|
-
folderId: folder.id,
|
|
191
|
-
})
|
|
192
|
-
const folderActive = isNavActive(pathname, nav, "folder", folder.id)
|
|
193
|
-
|
|
194
|
-
return (
|
|
195
|
-
<Collapsible defaultOpen={depth < 1} className="group">
|
|
196
|
-
<div
|
|
197
|
-
className={cn(
|
|
198
|
-
"group/row flex min-w-0 items-center rounded-md px-2 transition-colors",
|
|
199
|
-
folderActive
|
|
200
|
-
? "bg-sidebar-accent text-sidebar-accent-foreground"
|
|
201
|
-
: "hover:bg-sidebar-accent/50",
|
|
202
|
-
)}
|
|
203
|
-
>
|
|
204
|
-
<div style={{ width: indent }} className="shrink-0" aria-hidden />
|
|
205
|
-
{hasSubfolders ? (
|
|
206
|
-
<CollapsibleTrigger asChild>
|
|
207
|
-
<button
|
|
208
|
-
type="button"
|
|
209
|
-
className="flex size-8 shrink-0 items-center justify-center text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
210
|
-
aria-label={`${folder.name} — expand or collapse`}
|
|
211
|
-
>
|
|
212
|
-
<i
|
|
213
|
-
className="fa-light fa-chevron-right text-xs transition-transform duration-150 group-data-[state=open]:rotate-90"
|
|
214
|
-
aria-hidden
|
|
215
|
-
/>
|
|
216
|
-
</button>
|
|
217
|
-
</CollapsibleTrigger>
|
|
218
|
-
) : (
|
|
219
|
-
<div className="size-8 shrink-0" aria-hidden />
|
|
220
|
-
)}
|
|
221
|
-
<Tip label={folder.name} side="right">
|
|
222
|
-
<Link
|
|
223
|
-
href={folderHref}
|
|
224
|
-
scroll={false}
|
|
225
|
-
aria-current={folderActive ? "page" : undefined}
|
|
226
|
-
className={cn(
|
|
227
|
-
"flex min-w-0 flex-1 items-center gap-2 py-1.5 text-left text-sm transition-colors",
|
|
228
|
-
"rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
|
|
229
|
-
folderActive ? "font-medium" : "text-sidebar-foreground",
|
|
230
|
-
)}
|
|
231
|
-
>
|
|
232
|
-
<i
|
|
233
|
-
className={cn("fa-light shrink-0 text-sm", folder.icon, QUESTION_BANK_FOLDER_ICON_COLORS[folder.colorKey])}
|
|
234
|
-
aria-hidden
|
|
235
|
-
/>
|
|
236
|
-
<span className="min-w-0 flex-1 truncate leading-tight">{folder.name}</span>
|
|
237
|
-
{hasSubfolders ? (
|
|
238
|
-
<span className="shrink-0 text-xs tabular-nums text-muted-foreground">{childFolders.length}</span>
|
|
239
|
-
) : null}
|
|
240
|
-
</Link>
|
|
241
|
-
</Tip>
|
|
242
|
-
{canManageFolders ? (
|
|
243
|
-
<DropdownMenu>
|
|
244
|
-
<Tip label={`Folder actions for ${folder.name}`} side="right">
|
|
245
|
-
<DropdownMenuTrigger asChild>
|
|
246
|
-
<Button
|
|
247
|
-
type="button"
|
|
248
|
-
size="icon-xs"
|
|
249
|
-
variant="ghost"
|
|
250
|
-
aria-label={`Folder actions for ${folder.name}`}
|
|
251
|
-
className="shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover/row:opacity-100 group-focus-within/row:opacity-100"
|
|
252
|
-
onClick={event => event.stopPropagation()}
|
|
253
|
-
>
|
|
254
|
-
<i className="fa-light fa-ellipsis text-xs" aria-hidden="true" />
|
|
255
|
-
</Button>
|
|
256
|
-
</DropdownMenuTrigger>
|
|
257
|
-
</Tip>
|
|
258
|
-
<DropdownMenuContent align="end">
|
|
259
|
-
<DropdownMenuItem
|
|
260
|
-
onSelect={() => {
|
|
261
|
-
window.setTimeout(() => onAddSubfolder(folder.id), 0)
|
|
262
|
-
}}
|
|
263
|
-
>
|
|
264
|
-
<i className="fa-light fa-plus text-xs" aria-hidden="true" />
|
|
265
|
-
Add folder
|
|
266
|
-
</DropdownMenuItem>
|
|
267
|
-
<DropdownMenuItem
|
|
268
|
-
onSelect={() => {
|
|
269
|
-
window.setTimeout(() => onCustomizeFolder(folder), 0)
|
|
270
|
-
}}
|
|
271
|
-
>
|
|
272
|
-
<i className="fa-light fa-wand-magic-sparkles text-xs" aria-hidden="true" />
|
|
273
|
-
Customize
|
|
274
|
-
</DropdownMenuItem>
|
|
275
|
-
<DropdownMenuItem
|
|
276
|
-
disabled={!canManageAccess}
|
|
277
|
-
onSelect={() => {
|
|
278
|
-
window.setTimeout(() => onManageAccess(), 0)
|
|
279
|
-
}}
|
|
280
|
-
>
|
|
281
|
-
<i className="fa-light fa-user-gear text-xs" aria-hidden="true" />
|
|
282
|
-
Manage access
|
|
283
|
-
</DropdownMenuItem>
|
|
284
|
-
<DropdownMenuSeparator />
|
|
285
|
-
<DropdownMenuItem
|
|
286
|
-
variant="destructive"
|
|
287
|
-
onSelect={() => {
|
|
288
|
-
window.setTimeout(() => onDeleteFolder(folder), 0)
|
|
289
|
-
}}
|
|
290
|
-
>
|
|
291
|
-
<i className="fa-light fa-trash text-xs" aria-hidden="true" />
|
|
292
|
-
Delete
|
|
293
|
-
</DropdownMenuItem>
|
|
294
|
-
</DropdownMenuContent>
|
|
295
|
-
</DropdownMenu>
|
|
296
|
-
) : null}
|
|
297
|
-
</div>
|
|
298
|
-
{hasSubfolders ? (
|
|
299
|
-
<CollapsibleContent>
|
|
300
|
-
{childFolders.map(child => (
|
|
301
|
-
<PanelFolderBranch
|
|
302
|
-
key={child.id}
|
|
303
|
-
folder={child}
|
|
304
|
-
folders={folders}
|
|
305
|
-
depth={depth + 1}
|
|
306
|
-
pathname={pathname}
|
|
307
|
-
hubSearchParams={hubSearchParams}
|
|
308
|
-
nav={nav}
|
|
309
|
-
canManageFolders={canManageFolders}
|
|
310
|
-
canManageAccess={canManageAccess}
|
|
311
|
-
onAddSubfolder={onAddSubfolder}
|
|
312
|
-
onCustomizeFolder={onCustomizeFolder}
|
|
313
|
-
onManageAccess={onManageAccess}
|
|
314
|
-
onDeleteFolder={onDeleteFolder}
|
|
315
|
-
/>
|
|
316
|
-
))}
|
|
317
|
-
</CollapsibleContent>
|
|
318
|
-
) : null}
|
|
319
|
-
</Collapsible>
|
|
320
|
-
)
|
|
321
|
-
}
|
|
322
|
-
|
|
323
125
|
export function QuestionBankSecondaryNav() {
|
|
324
126
|
const pathname = usePathname()
|
|
325
127
|
const router = useRouter()
|
|
@@ -340,7 +142,7 @@ export function QuestionBankSecondaryNav() {
|
|
|
340
142
|
const canManageFolders = questionBankFolderBridge != null
|
|
341
143
|
const canManageAccess = questionBankAccessBridge != null
|
|
342
144
|
|
|
343
|
-
/** Favorites is a primary nav row (with All / My
|
|
145
|
+
/** Favorites is a primary nav row (with All / My), not under “Folders”. */
|
|
344
146
|
const folderTreeRoots = React.useMemo(
|
|
345
147
|
() =>
|
|
346
148
|
folders
|
|
@@ -518,32 +320,25 @@ export function QuestionBankSecondaryNav() {
|
|
|
518
320
|
<ul className="flex flex-1 flex-col items-center gap-1 overflow-y-auto px-1 py-2" role="list">
|
|
519
321
|
<IconNavRow
|
|
520
322
|
href={questionBankHubScopeHref(pathname, searchParams, { scope: "all" })}
|
|
521
|
-
active={
|
|
323
|
+
active={isQuestionBankNavActive(pathname, nav, "all")}
|
|
522
324
|
iconClass="fa-table-list"
|
|
523
325
|
label="All questions"
|
|
524
326
|
onClick={() => openPanel("question-bank")}
|
|
525
327
|
/>
|
|
526
328
|
<IconNavRow
|
|
527
329
|
href={questionBankHubScopeHref(pathname, searchParams, { scope: "my" })}
|
|
528
|
-
active={
|
|
330
|
+
active={isQuestionBankNavActive(pathname, nav, "my")}
|
|
529
331
|
iconClass="fa-user"
|
|
530
332
|
label="My questions"
|
|
531
333
|
onClick={() => openPanel("question-bank")}
|
|
532
334
|
/>
|
|
533
335
|
<IconNavRow
|
|
534
336
|
href={questionBankFavoritesFolderHref(pathname, searchParams)}
|
|
535
|
-
active={
|
|
337
|
+
active={isQuestionBankNavActive(pathname, nav, "folder", QUESTION_BANK_FAVORITES_FOLDER_ID)}
|
|
536
338
|
iconClass="fa-star"
|
|
537
339
|
label="Favorites"
|
|
538
340
|
onClick={() => openPanel("question-bank")}
|
|
539
341
|
/>
|
|
540
|
-
<IconNavRow
|
|
541
|
-
href={questionBankSearchLandingNavHref(nav, searchParams)}
|
|
542
|
-
active={isQuestionBankSearchNavActive(pathname, nav)}
|
|
543
|
-
iconClass="fa-magnifying-glass"
|
|
544
|
-
label="Search"
|
|
545
|
-
onClick={() => openPanel("question-bank")}
|
|
546
|
-
/>
|
|
547
342
|
<li className="flex w-full justify-center pt-1" role="none">
|
|
548
343
|
<DropdownMenu>
|
|
549
344
|
<DropdownMenuTrigger asChild>
|
|
@@ -566,7 +361,7 @@ export function QuestionBankSecondaryNav() {
|
|
|
566
361
|
scope: "folder",
|
|
567
362
|
folderId: folder.id,
|
|
568
363
|
})
|
|
569
|
-
const active =
|
|
364
|
+
const active = isQuestionBankNavActive(pathname, nav, "folder", folder.id)
|
|
570
365
|
return (
|
|
571
366
|
<DropdownMenuItem key={folder.id} asChild>
|
|
572
367
|
<Link
|
|
@@ -613,31 +408,24 @@ export function QuestionBankSecondaryNav() {
|
|
|
613
408
|
<ul className="space-y-0.5" role="list">
|
|
614
409
|
<NavRow
|
|
615
410
|
href={questionBankHubScopeHref(pathname, searchParams, { scope: "all" })}
|
|
616
|
-
active={
|
|
411
|
+
active={isQuestionBankNavActive(pathname, nav, "all")}
|
|
617
412
|
iconClass="fa-table-list"
|
|
618
413
|
label="All questions"
|
|
619
414
|
onClick={() => openPanel("question-bank")}
|
|
620
415
|
/>
|
|
621
416
|
<NavRow
|
|
622
417
|
href={questionBankHubScopeHref(pathname, searchParams, { scope: "my" })}
|
|
623
|
-
active={
|
|
418
|
+
active={isQuestionBankNavActive(pathname, nav, "my")}
|
|
624
419
|
iconClass="fa-user"
|
|
625
420
|
label="My questions"
|
|
626
421
|
/>
|
|
627
422
|
<NavRow
|
|
628
423
|
href={questionBankFavoritesFolderHref(pathname, searchParams)}
|
|
629
|
-
active={
|
|
424
|
+
active={isQuestionBankNavActive(pathname, nav, "folder", QUESTION_BANK_FAVORITES_FOLDER_ID)}
|
|
630
425
|
iconClass="fa-star"
|
|
631
426
|
label="Favorites"
|
|
632
427
|
onClick={() => openPanel("question-bank")}
|
|
633
428
|
/>
|
|
634
|
-
<NavRow
|
|
635
|
-
href={questionBankSearchLandingNavHref(nav, searchParams)}
|
|
636
|
-
active={isQuestionBankSearchNavActive(pathname, nav)}
|
|
637
|
-
iconClass="fa-magnifying-glass"
|
|
638
|
-
label="Search"
|
|
639
|
-
onClick={() => openPanel("question-bank")}
|
|
640
|
-
/>
|
|
641
429
|
<li role="presentation" className="select-none">
|
|
642
430
|
<div className="flex items-center justify-between gap-2 px-2 pt-3 pb-1">
|
|
643
431
|
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground/70">
|
|
@@ -659,11 +447,10 @@ export function QuestionBankSecondaryNav() {
|
|
|
659
447
|
</div>
|
|
660
448
|
</li>
|
|
661
449
|
{folderTreeRoots.map(folder => (
|
|
662
|
-
<li key={folder.id} className="min-w-0">
|
|
663
|
-
<
|
|
450
|
+
<li key={folder.id} className="min-w-0 w-full list-none">
|
|
451
|
+
<QuestionBankFolderTreeBranch
|
|
664
452
|
folder={folder}
|
|
665
453
|
folders={folders}
|
|
666
|
-
depth={0}
|
|
667
454
|
pathname={pathname}
|
|
668
455
|
hubSearchParams={searchParams}
|
|
669
456
|
nav={nav}
|
|
@@ -898,7 +898,11 @@ export const QuestionBankTable = React.forwardRef<
|
|
|
898
898
|
return (
|
|
899
899
|
<div className="flex min-h-0 flex-1 flex-col">
|
|
900
900
|
{sharedToolbar}
|
|
901
|
-
<QuestionBankListView
|
|
901
|
+
<QuestionBankListView
|
|
902
|
+
rows={tableState.rows as QuestionBankItem[]}
|
|
903
|
+
onToggleFavorite={toggleFavorite}
|
|
904
|
+
onRowActivate={row => tableState.toggleRow(row.id)}
|
|
905
|
+
/>
|
|
902
906
|
</div>
|
|
903
907
|
)
|
|
904
908
|
}
|
|
@@ -911,6 +915,7 @@ export const QuestionBankTable = React.forwardRef<
|
|
|
911
915
|
rows={tableState.rows as QuestionBankItem[]}
|
|
912
916
|
groupByColumnKey={questionBankBoardGroupKey}
|
|
913
917
|
onToggleFavorite={toggleFavorite}
|
|
918
|
+
onRowActivate={row => tableState.toggleRow(row.id)}
|
|
914
919
|
/>
|
|
915
920
|
</div>
|
|
916
921
|
)
|