@hed-hog/lms 0.0.312 → 0.0.315
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/dist/class-group/class-group.controller.d.ts +2 -2
- package/dist/class-group/class-group.service.d.ts +2 -2
- package/dist/enterprise/dto/enterprise-profile.dto.d.ts +13 -0
- package/dist/enterprise/dto/enterprise-profile.dto.d.ts.map +1 -0
- package/dist/enterprise/dto/enterprise-profile.dto.js +3 -0
- package/dist/enterprise/dto/enterprise-profile.dto.js.map +1 -0
- package/dist/enterprise/enterprise.controller.d.ts +3 -0
- package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
- package/dist/enterprise/enterprise.controller.js +14 -0
- package/dist/enterprise/enterprise.controller.js.map +1 -1
- package/dist/enterprise/enterprise.service.d.ts +3 -0
- package/dist/enterprise/enterprise.service.d.ts.map +1 -1
- package/dist/enterprise/enterprise.service.js +128 -1
- package/dist/enterprise/enterprise.service.js.map +1 -1
- package/dist/instructor/instructor.controller.d.ts +23 -0
- package/dist/instructor/instructor.controller.d.ts.map +1 -1
- package/dist/instructor/instructor.controller.js +41 -0
- package/dist/instructor/instructor.controller.js.map +1 -1
- package/dist/instructor/instructor.service.d.ts +25 -0
- package/dist/instructor/instructor.service.d.ts.map +1 -1
- package/dist/instructor/instructor.service.js +126 -8
- package/dist/instructor/instructor.service.js.map +1 -1
- package/hedhog/data/menu.yaml +23 -7
- package/hedhog/data/role.yaml +9 -1
- package/hedhog/data/route.yaml +54 -0
- package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +2 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +44 -44
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +362 -362
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +111 -111
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -134
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -113
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -314
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +62 -62
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +173 -173
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +58 -58
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +51 -51
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +276 -276
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +1216 -1216
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +1824 -1824
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +443 -443
- package/hedhog/frontend/app/courses/[id]/structure/_components/highlighted-text.tsx.ejs +40 -40
- package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +185 -185
- package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +264 -264
- package/hedhog/frontend/app/courses/[id]/structure/_components/search-filter.tsx.ejs +95 -95
- package/hedhog/frontend/app/courses/[id]/structure/_components/session-picker-dialog.tsx.ejs +73 -73
- package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +136 -136
- package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +80 -80
- package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +949 -949
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +525 -525
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-helpers.ts.ejs +181 -181
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-course.tsx.ejs +51 -51
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +271 -271
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +167 -167
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +108 -108
- package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs +318 -318
- package/hedhog/frontend/app/courses/[id]/structure/page.tsx.ejs +10 -10
- package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +2 -1
- package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +438 -0
- package/hedhog/frontend/app/instructors/_components/instructor-types.ts.ejs +40 -0
- package/hedhog/frontend/app/instructors/page.tsx.ejs +696 -0
- package/hedhog/frontend/app/training/page.tsx.ejs +2339 -0
- package/hedhog/table/enterprise_user.yaml +1 -1
- package/package.json +8 -8
- package/src/enterprise/dto/enterprise-profile.dto.ts +17 -0
- package/src/enterprise/enterprise.controller.ts +9 -1
- package/src/enterprise/enterprise.service.ts +147 -4
- package/src/instructor/instructor.controller.ts +36 -9
- package/src/instructor/instructor.service.ts +140 -10
|
@@ -1,167 +1,167 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { Input } from '@/components/ui/input';
|
|
4
|
-
import { cn } from '@/lib/utils';
|
|
5
|
-
import { ChevronDown, ChevronRight, Layers } from 'lucide-react';
|
|
6
|
-
import { useEffect, useRef } from 'react';
|
|
7
|
-
import { useUpdateSessionMutation } from '../_data/use-course-structure-mutations';
|
|
8
|
-
import { HighlightedText } from './highlighted-text';
|
|
9
|
-
import { useStructureStore } from './store';
|
|
10
|
-
import type { Session } from './types';
|
|
11
|
-
|
|
12
|
-
interface TreeRowSessionProps {
|
|
13
|
-
data: Session;
|
|
14
|
-
lessonCount: number;
|
|
15
|
-
isExpanded: boolean;
|
|
16
|
-
isSelected: boolean;
|
|
17
|
-
isActive: boolean;
|
|
18
|
-
query: string;
|
|
19
|
-
isMatched: boolean;
|
|
20
|
-
onClick: (e: React.MouseEvent) => void;
|
|
21
|
-
onToggleExpand: (e: React.MouseEvent) => void;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function TreeRowSession({
|
|
25
|
-
data,
|
|
26
|
-
lessonCount,
|
|
27
|
-
isExpanded,
|
|
28
|
-
isSelected,
|
|
29
|
-
isActive,
|
|
30
|
-
query,
|
|
31
|
-
onClick,
|
|
32
|
-
onToggleExpand,
|
|
33
|
-
}: TreeRowSessionProps) {
|
|
34
|
-
const inlineRenamingId = useStructureStore((s) => s.inlineRenamingId);
|
|
35
|
-
const cancelRename = useStructureStore((s) => s.cancelRename);
|
|
36
|
-
const startRename = useStructureStore((s) => s.startRename);
|
|
37
|
-
const renameItem = useStructureStore((s) => s.renameItem);
|
|
38
|
-
|
|
39
|
-
const updateSession = useUpdateSessionMutation();
|
|
40
|
-
|
|
41
|
-
const isRenaming = inlineRenamingId === data.id;
|
|
42
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
43
|
-
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
if (isRenaming) {
|
|
46
|
-
inputRef.current?.focus();
|
|
47
|
-
inputRef.current?.select();
|
|
48
|
-
}
|
|
49
|
-
}, [isRenaming]);
|
|
50
|
-
|
|
51
|
-
function commitAndPersist(newTitle: string) {
|
|
52
|
-
const trimmed = newTitle.trim() || data.title;
|
|
53
|
-
renameItem(data.id, trimmed);
|
|
54
|
-
cancelRename();
|
|
55
|
-
updateSession.mutate({
|
|
56
|
-
sessionId: data.id,
|
|
57
|
-
formValues: {
|
|
58
|
-
title: trimmed,
|
|
59
|
-
duration: data.duration,
|
|
60
|
-
code: data.code,
|
|
61
|
-
description: '',
|
|
62
|
-
visibility: data.visibility ?? 'publico',
|
|
63
|
-
published: data.published ?? false,
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
|
|
69
|
-
e.stopPropagation();
|
|
70
|
-
if (e.key === 'Enter') {
|
|
71
|
-
commitAndPersist(inputRef.current?.value ?? data.title);
|
|
72
|
-
} else if (e.key === 'Escape') {
|
|
73
|
-
cancelRename();
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const durationH = Math.floor((data.duration ?? 0) / 60);
|
|
78
|
-
const durationM = (data.duration ?? 0) % 60;
|
|
79
|
-
const durationLabel =
|
|
80
|
-
durationH > 0
|
|
81
|
-
? `${durationH}h${durationM > 0 ? ` ${durationM}m` : ''}`
|
|
82
|
-
: `${durationM}m`;
|
|
83
|
-
|
|
84
|
-
return (
|
|
85
|
-
<div
|
|
86
|
-
onClick={isRenaming ? undefined : onClick}
|
|
87
|
-
onDoubleClick={(e) => {
|
|
88
|
-
e.stopPropagation();
|
|
89
|
-
startRename(data.id);
|
|
90
|
-
}}
|
|
91
|
-
role="treeitem"
|
|
92
|
-
aria-expanded={isExpanded}
|
|
93
|
-
aria-selected={isActive}
|
|
94
|
-
className={cn(
|
|
95
|
-
'flex items-center gap-1.5 px-2 py-1.5 rounded-md cursor-pointer select-none text-sm group h-full',
|
|
96
|
-
'transition-colors duration-100',
|
|
97
|
-
isActive &&
|
|
98
|
-
'bg-accent text-accent-foreground ring-1 ring-inset ring-primary/20',
|
|
99
|
-
isSelected && !isActive && 'bg-accent/60',
|
|
100
|
-
!isActive && !isSelected && 'hover:bg-muted/60'
|
|
101
|
-
)}
|
|
102
|
-
>
|
|
103
|
-
{/* Expand/collapse chevron */}
|
|
104
|
-
<button
|
|
105
|
-
className="shrink-0 text-muted-foreground hover:text-foreground transition-colors p-0.5 rounded focus-visible:ring-1 focus-visible:ring-ring"
|
|
106
|
-
onClick={onToggleExpand}
|
|
107
|
-
aria-label={isExpanded ? 'Recolher sessão' : 'Expandir sessão'}
|
|
108
|
-
tabIndex={-1}
|
|
109
|
-
>
|
|
110
|
-
{isExpanded ? (
|
|
111
|
-
<ChevronDown className="size-3.5" />
|
|
112
|
-
) : (
|
|
113
|
-
<ChevronRight className="size-3.5" />
|
|
114
|
-
)}
|
|
115
|
-
</button>
|
|
116
|
-
|
|
117
|
-
{/* Session icon */}
|
|
118
|
-
<Layers
|
|
119
|
-
className="size-3.5 shrink-0 text-blue-500 dark:text-blue-400"
|
|
120
|
-
aria-hidden
|
|
121
|
-
/>
|
|
122
|
-
|
|
123
|
-
{/* Inline rename or title */}
|
|
124
|
-
{isRenaming ? (
|
|
125
|
-
<Input
|
|
126
|
-
ref={inputRef}
|
|
127
|
-
defaultValue={data.title}
|
|
128
|
-
className="h-5 text-xs px-1 py-0 flex-1 min-w-0"
|
|
129
|
-
onClick={(e) => e.stopPropagation()}
|
|
130
|
-
onBlur={(e) => commitAndPersist(e.target.value)}
|
|
131
|
-
onKeyDown={handleKeyDown}
|
|
132
|
-
aria-label="Renomear sessão"
|
|
133
|
-
/>
|
|
134
|
-
) : (
|
|
135
|
-
<span className="truncate flex-1 font-medium leading-tight">
|
|
136
|
-
<HighlightedText text={data.title} query={query} />
|
|
137
|
-
</span>
|
|
138
|
-
)}
|
|
139
|
-
|
|
140
|
-
{/* Meta badges (hidden during rename) */}
|
|
141
|
-
{!isRenaming && (
|
|
142
|
-
<div className="flex items-center gap-1 shrink-0">
|
|
143
|
-
{/* Duration - show on hover */}
|
|
144
|
-
{data.duration > 0 && (
|
|
145
|
-
<span className="text-[0.6rem] text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity tabular-nums">
|
|
146
|
-
{durationLabel}
|
|
147
|
-
</span>
|
|
148
|
-
)}
|
|
149
|
-
{/* Lesson count - always visible */}
|
|
150
|
-
<span className="text-[0.62rem] font-medium tabular-nums text-muted-foreground min-w-[1.5rem] text-right">
|
|
151
|
-
{lessonCount}
|
|
152
|
-
</span>
|
|
153
|
-
{/* Published status dot */}
|
|
154
|
-
<span
|
|
155
|
-
className={cn(
|
|
156
|
-
'size-1.5 rounded-full shrink-0',
|
|
157
|
-
data.published === false
|
|
158
|
-
? 'bg-muted-foreground/30'
|
|
159
|
-
: 'bg-emerald-500'
|
|
160
|
-
)}
|
|
161
|
-
title={data.published === false ? 'Rascunho' : 'Publicada'}
|
|
162
|
-
/>
|
|
163
|
-
</div>
|
|
164
|
-
)}
|
|
165
|
-
</div>
|
|
166
|
-
);
|
|
167
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Input } from '@/components/ui/input';
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
|
+
import { ChevronDown, ChevronRight, Layers } from 'lucide-react';
|
|
6
|
+
import { useEffect, useRef } from 'react';
|
|
7
|
+
import { useUpdateSessionMutation } from '../_data/use-course-structure-mutations';
|
|
8
|
+
import { HighlightedText } from './highlighted-text';
|
|
9
|
+
import { useStructureStore } from './store';
|
|
10
|
+
import type { Session } from './types';
|
|
11
|
+
|
|
12
|
+
interface TreeRowSessionProps {
|
|
13
|
+
data: Session;
|
|
14
|
+
lessonCount: number;
|
|
15
|
+
isExpanded: boolean;
|
|
16
|
+
isSelected: boolean;
|
|
17
|
+
isActive: boolean;
|
|
18
|
+
query: string;
|
|
19
|
+
isMatched: boolean;
|
|
20
|
+
onClick: (e: React.MouseEvent) => void;
|
|
21
|
+
onToggleExpand: (e: React.MouseEvent) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function TreeRowSession({
|
|
25
|
+
data,
|
|
26
|
+
lessonCount,
|
|
27
|
+
isExpanded,
|
|
28
|
+
isSelected,
|
|
29
|
+
isActive,
|
|
30
|
+
query,
|
|
31
|
+
onClick,
|
|
32
|
+
onToggleExpand,
|
|
33
|
+
}: TreeRowSessionProps) {
|
|
34
|
+
const inlineRenamingId = useStructureStore((s) => s.inlineRenamingId);
|
|
35
|
+
const cancelRename = useStructureStore((s) => s.cancelRename);
|
|
36
|
+
const startRename = useStructureStore((s) => s.startRename);
|
|
37
|
+
const renameItem = useStructureStore((s) => s.renameItem);
|
|
38
|
+
|
|
39
|
+
const updateSession = useUpdateSessionMutation();
|
|
40
|
+
|
|
41
|
+
const isRenaming = inlineRenamingId === data.id;
|
|
42
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (isRenaming) {
|
|
46
|
+
inputRef.current?.focus();
|
|
47
|
+
inputRef.current?.select();
|
|
48
|
+
}
|
|
49
|
+
}, [isRenaming]);
|
|
50
|
+
|
|
51
|
+
function commitAndPersist(newTitle: string) {
|
|
52
|
+
const trimmed = newTitle.trim() || data.title;
|
|
53
|
+
renameItem(data.id, trimmed);
|
|
54
|
+
cancelRename();
|
|
55
|
+
updateSession.mutate({
|
|
56
|
+
sessionId: data.id,
|
|
57
|
+
formValues: {
|
|
58
|
+
title: trimmed,
|
|
59
|
+
duration: data.duration,
|
|
60
|
+
code: data.code,
|
|
61
|
+
description: '',
|
|
62
|
+
visibility: data.visibility ?? 'publico',
|
|
63
|
+
published: data.published ?? false,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
|
|
69
|
+
e.stopPropagation();
|
|
70
|
+
if (e.key === 'Enter') {
|
|
71
|
+
commitAndPersist(inputRef.current?.value ?? data.title);
|
|
72
|
+
} else if (e.key === 'Escape') {
|
|
73
|
+
cancelRename();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const durationH = Math.floor((data.duration ?? 0) / 60);
|
|
78
|
+
const durationM = (data.duration ?? 0) % 60;
|
|
79
|
+
const durationLabel =
|
|
80
|
+
durationH > 0
|
|
81
|
+
? `${durationH}h${durationM > 0 ? ` ${durationM}m` : ''}`
|
|
82
|
+
: `${durationM}m`;
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div
|
|
86
|
+
onClick={isRenaming ? undefined : onClick}
|
|
87
|
+
onDoubleClick={(e) => {
|
|
88
|
+
e.stopPropagation();
|
|
89
|
+
startRename(data.id);
|
|
90
|
+
}}
|
|
91
|
+
role="treeitem"
|
|
92
|
+
aria-expanded={isExpanded}
|
|
93
|
+
aria-selected={isActive}
|
|
94
|
+
className={cn(
|
|
95
|
+
'flex items-center gap-1.5 px-2 py-1.5 rounded-md cursor-pointer select-none text-sm group h-full',
|
|
96
|
+
'transition-colors duration-100',
|
|
97
|
+
isActive &&
|
|
98
|
+
'bg-accent text-accent-foreground ring-1 ring-inset ring-primary/20',
|
|
99
|
+
isSelected && !isActive && 'bg-accent/60',
|
|
100
|
+
!isActive && !isSelected && 'hover:bg-muted/60'
|
|
101
|
+
)}
|
|
102
|
+
>
|
|
103
|
+
{/* Expand/collapse chevron */}
|
|
104
|
+
<button
|
|
105
|
+
className="shrink-0 text-muted-foreground hover:text-foreground transition-colors p-0.5 rounded focus-visible:ring-1 focus-visible:ring-ring"
|
|
106
|
+
onClick={onToggleExpand}
|
|
107
|
+
aria-label={isExpanded ? 'Recolher sessão' : 'Expandir sessão'}
|
|
108
|
+
tabIndex={-1}
|
|
109
|
+
>
|
|
110
|
+
{isExpanded ? (
|
|
111
|
+
<ChevronDown className="size-3.5" />
|
|
112
|
+
) : (
|
|
113
|
+
<ChevronRight className="size-3.5" />
|
|
114
|
+
)}
|
|
115
|
+
</button>
|
|
116
|
+
|
|
117
|
+
{/* Session icon */}
|
|
118
|
+
<Layers
|
|
119
|
+
className="size-3.5 shrink-0 text-blue-500 dark:text-blue-400"
|
|
120
|
+
aria-hidden
|
|
121
|
+
/>
|
|
122
|
+
|
|
123
|
+
{/* Inline rename or title */}
|
|
124
|
+
{isRenaming ? (
|
|
125
|
+
<Input
|
|
126
|
+
ref={inputRef}
|
|
127
|
+
defaultValue={data.title}
|
|
128
|
+
className="h-5 text-xs px-1 py-0 flex-1 min-w-0"
|
|
129
|
+
onClick={(e) => e.stopPropagation()}
|
|
130
|
+
onBlur={(e) => commitAndPersist(e.target.value)}
|
|
131
|
+
onKeyDown={handleKeyDown}
|
|
132
|
+
aria-label="Renomear sessão"
|
|
133
|
+
/>
|
|
134
|
+
) : (
|
|
135
|
+
<span className="truncate flex-1 font-medium leading-tight">
|
|
136
|
+
<HighlightedText text={data.title} query={query} />
|
|
137
|
+
</span>
|
|
138
|
+
)}
|
|
139
|
+
|
|
140
|
+
{/* Meta badges (hidden during rename) */}
|
|
141
|
+
{!isRenaming && (
|
|
142
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
143
|
+
{/* Duration - show on hover */}
|
|
144
|
+
{data.duration > 0 && (
|
|
145
|
+
<span className="text-[0.6rem] text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity tabular-nums">
|
|
146
|
+
{durationLabel}
|
|
147
|
+
</span>
|
|
148
|
+
)}
|
|
149
|
+
{/* Lesson count - always visible */}
|
|
150
|
+
<span className="text-[0.62rem] font-medium tabular-nums text-muted-foreground min-w-[1.5rem] text-right">
|
|
151
|
+
{lessonCount}
|
|
152
|
+
</span>
|
|
153
|
+
{/* Published status dot */}
|
|
154
|
+
<span
|
|
155
|
+
className={cn(
|
|
156
|
+
'size-1.5 rounded-full shrink-0',
|
|
157
|
+
data.published === false
|
|
158
|
+
? 'bg-muted-foreground/30'
|
|
159
|
+
: 'bg-emerald-500'
|
|
160
|
+
)}
|
|
161
|
+
title={data.published === false ? 'Rascunho' : 'Publicada'}
|
|
162
|
+
/>
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -1,108 +1,108 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useCallback } from 'react';
|
|
4
|
-
|
|
5
|
-
import { useStructureStore } from './store';
|
|
6
|
-
import { TreeContextMenu } from './tree-context-menu';
|
|
7
|
-
import { TreeRowCourse } from './tree-row-course';
|
|
8
|
-
import { TreeRowLesson } from './tree-row-lesson';
|
|
9
|
-
import { TreeRowSession } from './tree-row-session';
|
|
10
|
-
import type { FlatItem, Lesson, Session } from './types';
|
|
11
|
-
|
|
12
|
-
interface TreeRowProps {
|
|
13
|
-
item: FlatItem;
|
|
14
|
-
isActive: boolean;
|
|
15
|
-
isSelected: boolean;
|
|
16
|
-
query: string;
|
|
17
|
-
isMatched: boolean;
|
|
18
|
-
isEffectivelyExpanded: boolean;
|
|
19
|
-
lessonCountMap: Map<string, number>;
|
|
20
|
-
/** Full ordered visible list — required for SHIFT-range selection. */
|
|
21
|
-
visibleItems: FlatItem[];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function TreeRow({
|
|
25
|
-
item,
|
|
26
|
-
isActive,
|
|
27
|
-
isSelected,
|
|
28
|
-
query,
|
|
29
|
-
isMatched,
|
|
30
|
-
isEffectivelyExpanded,
|
|
31
|
-
lessonCountMap,
|
|
32
|
-
visibleItems,
|
|
33
|
-
}: TreeRowProps) {
|
|
34
|
-
const selectItem = useStructureStore((s) => s.selectItem);
|
|
35
|
-
const toggleExpand = useStructureStore((s) => s.toggleExpand);
|
|
36
|
-
const setMobileSheetOpen = useStructureStore((s) => s.setMobileSheetOpen);
|
|
37
|
-
|
|
38
|
-
const handleClick = useCallback(
|
|
39
|
-
(e: React.MouseEvent) => {
|
|
40
|
-
e.stopPropagation();
|
|
41
|
-
const modifiers = {
|
|
42
|
-
ctrl: e.ctrlKey || e.metaKey,
|
|
43
|
-
shift: e.shiftKey,
|
|
44
|
-
};
|
|
45
|
-
selectItem(item.id, item.type, modifiers, visibleItems);
|
|
46
|
-
// Close mobile sheet only on non-session items (sessions expand in-place)
|
|
47
|
-
if (item.type !== 'session' && !modifiers.ctrl && !modifiers.shift) {
|
|
48
|
-
setMobileSheetOpen(false);
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
[item.id, item.type, selectItem, setMobileSheetOpen, visibleItems]
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
const handleToggleExpand = useCallback(
|
|
55
|
-
(e: React.MouseEvent) => {
|
|
56
|
-
e.stopPropagation();
|
|
57
|
-
toggleExpand(item.id);
|
|
58
|
-
},
|
|
59
|
-
[item.id, toggleExpand]
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
if (item.type === 'course') {
|
|
63
|
-
return (
|
|
64
|
-
<TreeContextMenu item={item}>
|
|
65
|
-
<TreeRowCourse
|
|
66
|
-
data={item.data}
|
|
67
|
-
isSelected={isSelected}
|
|
68
|
-
isActive={isActive}
|
|
69
|
-
query={query}
|
|
70
|
-
isMatched={isMatched}
|
|
71
|
-
onClick={handleClick}
|
|
72
|
-
/>
|
|
73
|
-
</TreeContextMenu>
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (item.type === 'session') {
|
|
78
|
-
const lessonCount = lessonCountMap.get(item.id) ?? 0;
|
|
79
|
-
return (
|
|
80
|
-
<TreeContextMenu item={item}>
|
|
81
|
-
<TreeRowSession
|
|
82
|
-
data={item.data as Session}
|
|
83
|
-
lessonCount={lessonCount}
|
|
84
|
-
isExpanded={isEffectivelyExpanded}
|
|
85
|
-
isSelected={isSelected}
|
|
86
|
-
isActive={isActive}
|
|
87
|
-
query={query}
|
|
88
|
-
isMatched={isMatched}
|
|
89
|
-
onClick={handleClick}
|
|
90
|
-
onToggleExpand={handleToggleExpand}
|
|
91
|
-
/>
|
|
92
|
-
</TreeContextMenu>
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return (
|
|
97
|
-
<TreeContextMenu item={item}>
|
|
98
|
-
<TreeRowLesson
|
|
99
|
-
data={item.data as Lesson}
|
|
100
|
-
isSelected={isSelected}
|
|
101
|
-
isActive={isActive}
|
|
102
|
-
query={query}
|
|
103
|
-
isMatched={isMatched}
|
|
104
|
-
onClick={handleClick}
|
|
105
|
-
/>
|
|
106
|
-
</TreeContextMenu>
|
|
107
|
-
);
|
|
108
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback } from 'react';
|
|
4
|
+
|
|
5
|
+
import { useStructureStore } from './store';
|
|
6
|
+
import { TreeContextMenu } from './tree-context-menu';
|
|
7
|
+
import { TreeRowCourse } from './tree-row-course';
|
|
8
|
+
import { TreeRowLesson } from './tree-row-lesson';
|
|
9
|
+
import { TreeRowSession } from './tree-row-session';
|
|
10
|
+
import type { FlatItem, Lesson, Session } from './types';
|
|
11
|
+
|
|
12
|
+
interface TreeRowProps {
|
|
13
|
+
item: FlatItem;
|
|
14
|
+
isActive: boolean;
|
|
15
|
+
isSelected: boolean;
|
|
16
|
+
query: string;
|
|
17
|
+
isMatched: boolean;
|
|
18
|
+
isEffectivelyExpanded: boolean;
|
|
19
|
+
lessonCountMap: Map<string, number>;
|
|
20
|
+
/** Full ordered visible list — required for SHIFT-range selection. */
|
|
21
|
+
visibleItems: FlatItem[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function TreeRow({
|
|
25
|
+
item,
|
|
26
|
+
isActive,
|
|
27
|
+
isSelected,
|
|
28
|
+
query,
|
|
29
|
+
isMatched,
|
|
30
|
+
isEffectivelyExpanded,
|
|
31
|
+
lessonCountMap,
|
|
32
|
+
visibleItems,
|
|
33
|
+
}: TreeRowProps) {
|
|
34
|
+
const selectItem = useStructureStore((s) => s.selectItem);
|
|
35
|
+
const toggleExpand = useStructureStore((s) => s.toggleExpand);
|
|
36
|
+
const setMobileSheetOpen = useStructureStore((s) => s.setMobileSheetOpen);
|
|
37
|
+
|
|
38
|
+
const handleClick = useCallback(
|
|
39
|
+
(e: React.MouseEvent) => {
|
|
40
|
+
e.stopPropagation();
|
|
41
|
+
const modifiers = {
|
|
42
|
+
ctrl: e.ctrlKey || e.metaKey,
|
|
43
|
+
shift: e.shiftKey,
|
|
44
|
+
};
|
|
45
|
+
selectItem(item.id, item.type, modifiers, visibleItems);
|
|
46
|
+
// Close mobile sheet only on non-session items (sessions expand in-place)
|
|
47
|
+
if (item.type !== 'session' && !modifiers.ctrl && !modifiers.shift) {
|
|
48
|
+
setMobileSheetOpen(false);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
[item.id, item.type, selectItem, setMobileSheetOpen, visibleItems]
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const handleToggleExpand = useCallback(
|
|
55
|
+
(e: React.MouseEvent) => {
|
|
56
|
+
e.stopPropagation();
|
|
57
|
+
toggleExpand(item.id);
|
|
58
|
+
},
|
|
59
|
+
[item.id, toggleExpand]
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if (item.type === 'course') {
|
|
63
|
+
return (
|
|
64
|
+
<TreeContextMenu item={item}>
|
|
65
|
+
<TreeRowCourse
|
|
66
|
+
data={item.data}
|
|
67
|
+
isSelected={isSelected}
|
|
68
|
+
isActive={isActive}
|
|
69
|
+
query={query}
|
|
70
|
+
isMatched={isMatched}
|
|
71
|
+
onClick={handleClick}
|
|
72
|
+
/>
|
|
73
|
+
</TreeContextMenu>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (item.type === 'session') {
|
|
78
|
+
const lessonCount = lessonCountMap.get(item.id) ?? 0;
|
|
79
|
+
return (
|
|
80
|
+
<TreeContextMenu item={item}>
|
|
81
|
+
<TreeRowSession
|
|
82
|
+
data={item.data as Session}
|
|
83
|
+
lessonCount={lessonCount}
|
|
84
|
+
isExpanded={isEffectivelyExpanded}
|
|
85
|
+
isSelected={isSelected}
|
|
86
|
+
isActive={isActive}
|
|
87
|
+
query={query}
|
|
88
|
+
isMatched={isMatched}
|
|
89
|
+
onClick={handleClick}
|
|
90
|
+
onToggleExpand={handleToggleExpand}
|
|
91
|
+
/>
|
|
92
|
+
</TreeContextMenu>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<TreeContextMenu item={item}>
|
|
98
|
+
<TreeRowLesson
|
|
99
|
+
data={item.data as Lesson}
|
|
100
|
+
isSelected={isSelected}
|
|
101
|
+
isActive={isActive}
|
|
102
|
+
query={query}
|
|
103
|
+
isMatched={isMatched}
|
|
104
|
+
onClick={handleClick}
|
|
105
|
+
/>
|
|
106
|
+
</TreeContextMenu>
|
|
107
|
+
);
|
|
108
|
+
}
|