@hed-hog/lms 0.0.314 → 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,525 +1,525 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
ContextMenu,
|
|
5
|
-
ContextMenuContent,
|
|
6
|
-
ContextMenuItem,
|
|
7
|
-
ContextMenuLabel,
|
|
8
|
-
ContextMenuSeparator,
|
|
9
|
-
ContextMenuTrigger,
|
|
10
|
-
} from '@/components/ui/context-menu';
|
|
11
|
-
import {
|
|
12
|
-
ArrowDown,
|
|
13
|
-
ArrowUp,
|
|
14
|
-
BookOpen,
|
|
15
|
-
Clipboard,
|
|
16
|
-
Copy,
|
|
17
|
-
FolderOpen,
|
|
18
|
-
Layers,
|
|
19
|
-
Loader2,
|
|
20
|
-
Pencil,
|
|
21
|
-
Plus,
|
|
22
|
-
Trash2,
|
|
23
|
-
} from 'lucide-react';
|
|
24
|
-
import { toast } from 'sonner';
|
|
25
|
-
|
|
26
|
-
import {
|
|
27
|
-
useBulkDeleteMutation,
|
|
28
|
-
useCreateLessonMutation,
|
|
29
|
-
useCreateSessionMutation,
|
|
30
|
-
useDeleteLessonMutation,
|
|
31
|
-
useDeleteSessionMutation,
|
|
32
|
-
useDuplicateLessonMutation,
|
|
33
|
-
useDuplicateSessionMutation,
|
|
34
|
-
useMoveLessonsMutation,
|
|
35
|
-
usePasteLessonsMutation,
|
|
36
|
-
usePasteSessionsMutation,
|
|
37
|
-
useReorderSessionsMutation,
|
|
38
|
-
} from '../_data/use-course-structure-mutations';
|
|
39
|
-
import { useStructureStore } from './store';
|
|
40
|
-
import type { Course, FlatItem, Lesson, Session } from './types';
|
|
41
|
-
|
|
42
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
43
|
-
// Types
|
|
44
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
45
|
-
|
|
46
|
-
interface TreeContextMenuProps {
|
|
47
|
-
item: FlatItem;
|
|
48
|
-
children: React.ReactNode;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
52
|
-
// Sub-menus by item type
|
|
53
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
54
|
-
|
|
55
|
-
function CourseMenu({ data }: { data: Course }) {
|
|
56
|
-
const expandAll = useStructureStore((s) => s.expandAll);
|
|
57
|
-
const collapseAll = useStructureStore((s) => s.collapseAll);
|
|
58
|
-
const copiedType = useStructureStore((s) => s.copiedType);
|
|
59
|
-
const copiedIds = useStructureStore((s) => s.copiedIds);
|
|
60
|
-
|
|
61
|
-
const createSessionMutation = useCreateSessionMutation();
|
|
62
|
-
const pasteSessionsMutation = usePasteSessionsMutation();
|
|
63
|
-
const hasSessionClipboard = copiedType === 'session' && copiedIds.length > 0;
|
|
64
|
-
|
|
65
|
-
return (
|
|
66
|
-
<>
|
|
67
|
-
<ContextMenuLabel className="text-[0.65rem] text-muted-foreground px-2 py-1">
|
|
68
|
-
{data.title}
|
|
69
|
-
</ContextMenuLabel>
|
|
70
|
-
<ContextMenuSeparator />
|
|
71
|
-
|
|
72
|
-
<ContextMenuItem
|
|
73
|
-
disabled={createSessionMutation.isPending}
|
|
74
|
-
onSelect={() => {
|
|
75
|
-
createSessionMutation.mutate();
|
|
76
|
-
}}
|
|
77
|
-
>
|
|
78
|
-
{createSessionMutation.isPending ? (
|
|
79
|
-
<Loader2 className="size-3.5 mr-2 animate-spin" />
|
|
80
|
-
) : (
|
|
81
|
-
<Plus className="size-3.5 mr-2 text-muted-foreground" />
|
|
82
|
-
)}
|
|
83
|
-
Nova sessão
|
|
84
|
-
</ContextMenuItem>
|
|
85
|
-
|
|
86
|
-
<ContextMenuSeparator />
|
|
87
|
-
|
|
88
|
-
<ContextMenuItem
|
|
89
|
-
onSelect={() => {
|
|
90
|
-
expandAll();
|
|
91
|
-
toast('Tudo expandido');
|
|
92
|
-
}}
|
|
93
|
-
>
|
|
94
|
-
<BookOpen className="size-3.5 mr-2 text-muted-foreground" />
|
|
95
|
-
Expandir tudo
|
|
96
|
-
</ContextMenuItem>
|
|
97
|
-
|
|
98
|
-
<ContextMenuItem
|
|
99
|
-
onSelect={() => {
|
|
100
|
-
collapseAll();
|
|
101
|
-
toast('Tudo recolhido');
|
|
102
|
-
}}
|
|
103
|
-
>
|
|
104
|
-
<Layers className="size-3.5 mr-2 text-muted-foreground" />
|
|
105
|
-
Recolher tudo
|
|
106
|
-
</ContextMenuItem>
|
|
107
|
-
|
|
108
|
-
{hasSessionClipboard && (
|
|
109
|
-
<>
|
|
110
|
-
<ContextMenuSeparator />
|
|
111
|
-
<ContextMenuItem
|
|
112
|
-
disabled={pasteSessionsMutation.isPending}
|
|
113
|
-
onSelect={() => {
|
|
114
|
-
pasteSessionsMutation.mutate({ sessionIds: copiedIds });
|
|
115
|
-
}}
|
|
116
|
-
>
|
|
117
|
-
{pasteSessionsMutation.isPending ? (
|
|
118
|
-
<Loader2 className="size-3.5 mr-2 animate-spin" />
|
|
119
|
-
) : (
|
|
120
|
-
<Clipboard className="size-3.5 mr-2 text-muted-foreground" />
|
|
121
|
-
)}
|
|
122
|
-
Colar sessão{copiedIds.length > 1 ? ` (${copiedIds.length})` : ''}
|
|
123
|
-
</ContextMenuItem>
|
|
124
|
-
</>
|
|
125
|
-
)}
|
|
126
|
-
</>
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function SessionMenu({ data }: { data: Session }) {
|
|
131
|
-
const sessions = useStructureStore((s) => s.sessions);
|
|
132
|
-
const selectedIds = useStructureStore((s) => s.selectedIds);
|
|
133
|
-
const copiedType = useStructureStore((s) => s.copiedType);
|
|
134
|
-
const copiedIds = useStructureStore((s) => s.copiedIds);
|
|
135
|
-
const copyItems = useStructureStore((s) => s.copyItems);
|
|
136
|
-
const showConfirm = useStructureStore((s) => s.showConfirm);
|
|
137
|
-
const startRename = useStructureStore((s) => s.startRename);
|
|
138
|
-
|
|
139
|
-
const createLessonMutation = useCreateLessonMutation();
|
|
140
|
-
const deleteSessionMutation = useDeleteSessionMutation();
|
|
141
|
-
const duplicateSessionMutation = useDuplicateSessionMutation();
|
|
142
|
-
const pasteLessonsMutation = usePasteLessonsMutation();
|
|
143
|
-
const reorderSessionsMutation = useReorderSessionsMutation();
|
|
144
|
-
const reorderSessionsInStore = useStructureStore((s) => s.reorderSessions);
|
|
145
|
-
|
|
146
|
-
const sorted = [...sessions].sort((a, b) => a.order - b.order);
|
|
147
|
-
const idx = sorted.findIndex((s) => s.id === data.id);
|
|
148
|
-
const isFirst = idx === 0;
|
|
149
|
-
const isLast = idx === sorted.length - 1;
|
|
150
|
-
|
|
151
|
-
// Count selected sessions
|
|
152
|
-
const selectedSessionIds = sessions
|
|
153
|
-
.filter((s) => selectedIds.has(`session:${s.id}`))
|
|
154
|
-
.map((s) => s.id);
|
|
155
|
-
const multiSessions = selectedSessionIds.length > 1;
|
|
156
|
-
|
|
157
|
-
const hasLessonClipboard = copiedType === 'lesson' && copiedIds.length > 0;
|
|
158
|
-
|
|
159
|
-
return (
|
|
160
|
-
<>
|
|
161
|
-
<ContextMenuLabel className="text-[0.65rem] text-muted-foreground px-2 py-1">
|
|
162
|
-
{data.title}
|
|
163
|
-
</ContextMenuLabel>
|
|
164
|
-
<ContextMenuSeparator />
|
|
165
|
-
|
|
166
|
-
<ContextMenuItem
|
|
167
|
-
disabled={createLessonMutation.isPending}
|
|
168
|
-
onSelect={() => {
|
|
169
|
-
createLessonMutation.mutate({ sessionId: data.id });
|
|
170
|
-
}}
|
|
171
|
-
>
|
|
172
|
-
{createLessonMutation.isPending ? (
|
|
173
|
-
<Loader2 className="size-3.5 mr-2 animate-spin" />
|
|
174
|
-
) : (
|
|
175
|
-
<Plus className="size-3.5 mr-2 text-muted-foreground" />
|
|
176
|
-
)}
|
|
177
|
-
Nova aula nesta sessão
|
|
178
|
-
</ContextMenuItem>
|
|
179
|
-
|
|
180
|
-
<ContextMenuSeparator />
|
|
181
|
-
|
|
182
|
-
<ContextMenuItem onSelect={() => startRename(data.id)}>
|
|
183
|
-
<Pencil className="size-3.5 mr-2 text-muted-foreground" />
|
|
184
|
-
Renomear
|
|
185
|
-
</ContextMenuItem>
|
|
186
|
-
|
|
187
|
-
<ContextMenuItem
|
|
188
|
-
disabled={duplicateSessionMutation.isPending}
|
|
189
|
-
onSelect={() => {
|
|
190
|
-
duplicateSessionMutation.mutate({ sessionId: data.id });
|
|
191
|
-
}}
|
|
192
|
-
>
|
|
193
|
-
{duplicateSessionMutation.isPending ? (
|
|
194
|
-
<Loader2 className="size-3.5 mr-2 animate-spin" />
|
|
195
|
-
) : (
|
|
196
|
-
<Copy className="size-3.5 mr-2 text-muted-foreground" />
|
|
197
|
-
)}
|
|
198
|
-
Duplicar sessão
|
|
199
|
-
</ContextMenuItem>
|
|
200
|
-
|
|
201
|
-
<ContextMenuItem
|
|
202
|
-
onSelect={() => {
|
|
203
|
-
copyItems([data.id], 'session');
|
|
204
|
-
toast.success('Sessão copiada');
|
|
205
|
-
}}
|
|
206
|
-
>
|
|
207
|
-
<Copy className="size-3.5 mr-2 text-muted-foreground" />
|
|
208
|
-
Copiar sessão
|
|
209
|
-
</ContextMenuItem>
|
|
210
|
-
|
|
211
|
-
{multiSessions && (
|
|
212
|
-
<ContextMenuItem
|
|
213
|
-
onSelect={() => {
|
|
214
|
-
copyItems(selectedSessionIds, 'session');
|
|
215
|
-
toast.success(`${selectedSessionIds.length} sessões copiadas`);
|
|
216
|
-
}}
|
|
217
|
-
>
|
|
218
|
-
<Copy className="size-3.5 mr-2 text-muted-foreground" />
|
|
219
|
-
Copiar selecionadas ({selectedSessionIds.length})
|
|
220
|
-
</ContextMenuItem>
|
|
221
|
-
)}
|
|
222
|
-
|
|
223
|
-
{hasLessonClipboard && (
|
|
224
|
-
<ContextMenuItem
|
|
225
|
-
disabled={pasteLessonsMutation.isPending}
|
|
226
|
-
onSelect={() => {
|
|
227
|
-
pasteLessonsMutation.mutate({
|
|
228
|
-
targetSessionId: data.id,
|
|
229
|
-
lessonIds: copiedIds,
|
|
230
|
-
});
|
|
231
|
-
}}
|
|
232
|
-
>
|
|
233
|
-
{pasteLessonsMutation.isPending ? (
|
|
234
|
-
<Loader2 className="size-3.5 mr-2 animate-spin" />
|
|
235
|
-
) : (
|
|
236
|
-
<Clipboard className="size-3.5 mr-2 text-muted-foreground" />
|
|
237
|
-
)}
|
|
238
|
-
Colar aulas nesta sessão
|
|
239
|
-
{copiedIds.length > 1 ? ` (${copiedIds.length})` : ''}
|
|
240
|
-
</ContextMenuItem>
|
|
241
|
-
)}
|
|
242
|
-
|
|
243
|
-
<ContextMenuSeparator />
|
|
244
|
-
|
|
245
|
-
<ContextMenuItem
|
|
246
|
-
disabled={isFirst || reorderSessionsMutation.isPending}
|
|
247
|
-
onSelect={() => {
|
|
248
|
-
const previousSessions = [...sorted];
|
|
249
|
-
reorderSessionsInStore(idx, idx - 1);
|
|
250
|
-
const newSorted = [...sorted];
|
|
251
|
-
const [moved] = newSorted.splice(idx, 1);
|
|
252
|
-
newSorted.splice(idx - 1, 0, moved!);
|
|
253
|
-
reorderSessionsMutation.mutate({
|
|
254
|
-
orderedIds: newSorted.map((s) => s.id),
|
|
255
|
-
previousSessions,
|
|
256
|
-
});
|
|
257
|
-
}}
|
|
258
|
-
>
|
|
259
|
-
<ArrowUp className="size-3.5 mr-2 text-muted-foreground" />
|
|
260
|
-
Mover para cima
|
|
261
|
-
</ContextMenuItem>
|
|
262
|
-
|
|
263
|
-
<ContextMenuItem
|
|
264
|
-
disabled={isLast || reorderSessionsMutation.isPending}
|
|
265
|
-
onSelect={() => {
|
|
266
|
-
const previousSessions = [...sorted];
|
|
267
|
-
reorderSessionsInStore(idx, idx + 1);
|
|
268
|
-
const newSorted = [...sorted];
|
|
269
|
-
const [moved] = newSorted.splice(idx, 1);
|
|
270
|
-
newSorted.splice(idx + 1, 0, moved!);
|
|
271
|
-
reorderSessionsMutation.mutate({
|
|
272
|
-
orderedIds: newSorted.map((s) => s.id),
|
|
273
|
-
previousSessions,
|
|
274
|
-
});
|
|
275
|
-
}}
|
|
276
|
-
>
|
|
277
|
-
<ArrowDown className="size-3.5 mr-2 text-muted-foreground" />
|
|
278
|
-
Mover para baixo
|
|
279
|
-
</ContextMenuItem>
|
|
280
|
-
|
|
281
|
-
<ContextMenuSeparator />
|
|
282
|
-
|
|
283
|
-
<ContextMenuItem
|
|
284
|
-
className="text-destructive focus:text-destructive"
|
|
285
|
-
onSelect={() => {
|
|
286
|
-
showConfirm({
|
|
287
|
-
title: `Excluir "${data.title}"?`,
|
|
288
|
-
description:
|
|
289
|
-
'A sessão e todas as suas aulas serão excluídas permanentemente.',
|
|
290
|
-
onConfirm: () => {
|
|
291
|
-
deleteSessionMutation.mutate({ sessionId: data.id });
|
|
292
|
-
},
|
|
293
|
-
});
|
|
294
|
-
}}
|
|
295
|
-
>
|
|
296
|
-
<Trash2 className="size-3.5 mr-2" />
|
|
297
|
-
Excluir sessão
|
|
298
|
-
</ContextMenuItem>
|
|
299
|
-
</>
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function LessonMenu({ data }: { data: Lesson }) {
|
|
304
|
-
const lessons = useStructureStore((s) => s.lessons);
|
|
305
|
-
const sessions = useStructureStore((s) => s.sessions);
|
|
306
|
-
const selectedIds = useStructureStore((s) => s.selectedIds);
|
|
307
|
-
const copiedType = useStructureStore((s) => s.copiedType);
|
|
308
|
-
const copiedIds = useStructureStore((s) => s.copiedIds);
|
|
309
|
-
const copyItems = useStructureStore((s) => s.copyItems);
|
|
310
|
-
const showSessionPicker = useStructureStore((s) => s.showSessionPicker);
|
|
311
|
-
const showConfirm = useStructureStore((s) => s.showConfirm);
|
|
312
|
-
const startRename = useStructureStore((s) => s.startRename);
|
|
313
|
-
|
|
314
|
-
const deleteLessonMutation = useDeleteLessonMutation();
|
|
315
|
-
const bulkDeleteMutation = useBulkDeleteMutation();
|
|
316
|
-
const duplicateLessonMutation = useDuplicateLessonMutation();
|
|
317
|
-
const pasteLessonsMutation = usePasteLessonsMutation();
|
|
318
|
-
const moveLessonsMutation = useMoveLessonsMutation();
|
|
319
|
-
|
|
320
|
-
// Count selected lessons
|
|
321
|
-
const selectedLessonIds = lessons
|
|
322
|
-
.filter((l) => selectedIds.has(l.id))
|
|
323
|
-
.map((l) => l.id);
|
|
324
|
-
const multiLessons = selectedLessonIds.length > 1;
|
|
325
|
-
|
|
326
|
-
const hasLessonClipboard = copiedType === 'lesson' && copiedIds.length > 0;
|
|
327
|
-
const hasOtherSessions = sessions.some((ss) => ss.id !== data.sessionId);
|
|
328
|
-
|
|
329
|
-
// Ids to move when "Mover" is triggered
|
|
330
|
-
const idsToMove = multiLessons ? selectedLessonIds : [data.id];
|
|
331
|
-
|
|
332
|
-
return (
|
|
333
|
-
<>
|
|
334
|
-
<ContextMenuLabel className="text-[0.65rem] text-muted-foreground px-2 py-1">
|
|
335
|
-
{data.title}
|
|
336
|
-
</ContextMenuLabel>
|
|
337
|
-
<ContextMenuSeparator />
|
|
338
|
-
|
|
339
|
-
<ContextMenuItem onSelect={() => startRename(data.id)}>
|
|
340
|
-
<Pencil className="size-3.5 mr-2 text-muted-foreground" />
|
|
341
|
-
Renomear
|
|
342
|
-
</ContextMenuItem>
|
|
343
|
-
|
|
344
|
-
<ContextMenuSeparator />
|
|
345
|
-
|
|
346
|
-
<ContextMenuItem
|
|
347
|
-
disabled={duplicateLessonMutation.isPending}
|
|
348
|
-
onSelect={() => {
|
|
349
|
-
duplicateLessonMutation.mutate({
|
|
350
|
-
sessionId: data.sessionId,
|
|
351
|
-
lessonId: data.id,
|
|
352
|
-
});
|
|
353
|
-
}}
|
|
354
|
-
>
|
|
355
|
-
{duplicateLessonMutation.isPending ? (
|
|
356
|
-
<Loader2 className="size-3.5 mr-2 animate-spin" />
|
|
357
|
-
) : (
|
|
358
|
-
<Copy className="size-3.5 mr-2 text-muted-foreground" />
|
|
359
|
-
)}
|
|
360
|
-
Duplicar aula
|
|
361
|
-
</ContextMenuItem>
|
|
362
|
-
|
|
363
|
-
<ContextMenuItem
|
|
364
|
-
onSelect={() => {
|
|
365
|
-
copyItems([data.id], 'lesson');
|
|
366
|
-
toast.success('Aula copiada');
|
|
367
|
-
}}
|
|
368
|
-
>
|
|
369
|
-
<Copy className="size-3.5 mr-2 text-muted-foreground" />
|
|
370
|
-
Copiar aula
|
|
371
|
-
</ContextMenuItem>
|
|
372
|
-
|
|
373
|
-
{multiLessons && (
|
|
374
|
-
<ContextMenuItem
|
|
375
|
-
onSelect={() => {
|
|
376
|
-
copyItems(selectedLessonIds, 'lesson');
|
|
377
|
-
toast.success(`${selectedLessonIds.length} aulas copiadas`);
|
|
378
|
-
}}
|
|
379
|
-
>
|
|
380
|
-
<Copy className="size-3.5 mr-2 text-muted-foreground" />
|
|
381
|
-
Copiar selecionadas ({selectedLessonIds.length})
|
|
382
|
-
</ContextMenuItem>
|
|
383
|
-
)}
|
|
384
|
-
|
|
385
|
-
{hasLessonClipboard && (
|
|
386
|
-
<ContextMenuItem
|
|
387
|
-
disabled={pasteLessonsMutation.isPending}
|
|
388
|
-
onSelect={() => {
|
|
389
|
-
pasteLessonsMutation.mutate({
|
|
390
|
-
targetSessionId: data.sessionId,
|
|
391
|
-
lessonIds: copiedIds,
|
|
392
|
-
});
|
|
393
|
-
}}
|
|
394
|
-
>
|
|
395
|
-
{pasteLessonsMutation.isPending ? (
|
|
396
|
-
<Loader2 className="size-3.5 mr-2 animate-spin" />
|
|
397
|
-
) : (
|
|
398
|
-
<Clipboard className="size-3.5 mr-2 text-muted-foreground" />
|
|
399
|
-
)}
|
|
400
|
-
Colar nesta sessão
|
|
401
|
-
</ContextMenuItem>
|
|
402
|
-
)}
|
|
403
|
-
|
|
404
|
-
{hasOtherSessions && (
|
|
405
|
-
<>
|
|
406
|
-
<ContextMenuSeparator />
|
|
407
|
-
<ContextMenuItem
|
|
408
|
-
disabled={moveLessonsMutation.isPending}
|
|
409
|
-
onSelect={() => {
|
|
410
|
-
showSessionPicker({
|
|
411
|
-
title:
|
|
412
|
-
idsToMove.length > 1
|
|
413
|
-
? `Mover ${idsToMove.length} aulas para`
|
|
414
|
-
: 'Mover aula para',
|
|
415
|
-
excludeSessionId: data.sessionId,
|
|
416
|
-
onPick: (targetId) => {
|
|
417
|
-
// Snapshot for rollback
|
|
418
|
-
const previousLessons = [...lessons];
|
|
419
|
-
// Compute moves with sequential toIndex
|
|
420
|
-
const movingLessons = idsToMove
|
|
421
|
-
.map((id) => lessons.find((l) => l.id === id))
|
|
422
|
-
.filter(Boolean) as Lesson[];
|
|
423
|
-
const moves = movingLessons.map((l, i) => ({
|
|
424
|
-
lessonId: l.id,
|
|
425
|
-
fromSessionId: l.sessionId,
|
|
426
|
-
toSessionId: targetId,
|
|
427
|
-
toIndex: i,
|
|
428
|
-
}));
|
|
429
|
-
// Optimistic: update store immediately
|
|
430
|
-
useStructureStore.getState().moveLessons(idsToMove, targetId);
|
|
431
|
-
moveLessonsMutation.mutate({ moves, previousLessons });
|
|
432
|
-
toast.success(
|
|
433
|
-
idsToMove.length > 1
|
|
434
|
-
? `${idsToMove.length} aulas movidas`
|
|
435
|
-
: 'Aula movida'
|
|
436
|
-
);
|
|
437
|
-
},
|
|
438
|
-
});
|
|
439
|
-
}}
|
|
440
|
-
>
|
|
441
|
-
{moveLessonsMutation.isPending ? (
|
|
442
|
-
<Loader2 className="size-3.5 mr-2 animate-spin" />
|
|
443
|
-
) : (
|
|
444
|
-
<FolderOpen className="size-3.5 mr-2 text-muted-foreground" />
|
|
445
|
-
)}
|
|
446
|
-
{multiLessons
|
|
447
|
-
? `Mover ${idsToMove.length} aulas para...`
|
|
448
|
-
: 'Mover para outra sessão...'}
|
|
449
|
-
</ContextMenuItem>
|
|
450
|
-
</>
|
|
451
|
-
)}
|
|
452
|
-
|
|
453
|
-
<ContextMenuSeparator />
|
|
454
|
-
|
|
455
|
-
<ContextMenuItem
|
|
456
|
-
className="text-destructive focus:text-destructive"
|
|
457
|
-
onSelect={() => {
|
|
458
|
-
if (multiLessons) {
|
|
459
|
-
showConfirm({
|
|
460
|
-
title: `Excluir ${selectedLessonIds.length} aulas?`,
|
|
461
|
-
description: 'Esta ação não pode ser desfeita.',
|
|
462
|
-
onConfirm: () => {
|
|
463
|
-
const lessonPairs = selectedLessonIds
|
|
464
|
-
.map((id) => lessons.find((l) => l.id === id))
|
|
465
|
-
.filter(Boolean)
|
|
466
|
-
.map((l) => ({ lessonId: l!.id, sessionId: l!.sessionId }));
|
|
467
|
-
bulkDeleteMutation.mutate({
|
|
468
|
-
sessionIds: [],
|
|
469
|
-
lessons: lessonPairs,
|
|
470
|
-
});
|
|
471
|
-
},
|
|
472
|
-
});
|
|
473
|
-
} else {
|
|
474
|
-
showConfirm({
|
|
475
|
-
title: `Excluir "${data.title}"?`,
|
|
476
|
-
description: 'Esta ação não pode ser desfeita.',
|
|
477
|
-
onConfirm: () => {
|
|
478
|
-
deleteLessonMutation.mutate({
|
|
479
|
-
lessonId: data.id,
|
|
480
|
-
sessionId: data.sessionId,
|
|
481
|
-
});
|
|
482
|
-
},
|
|
483
|
-
});
|
|
484
|
-
}
|
|
485
|
-
}}
|
|
486
|
-
>
|
|
487
|
-
<Trash2 className="size-3.5 mr-2" />
|
|
488
|
-
{multiLessons
|
|
489
|
-
? `Excluir ${selectedLessonIds.length} aulas`
|
|
490
|
-
: 'Excluir aula'}
|
|
491
|
-
</ContextMenuItem>
|
|
492
|
-
</>
|
|
493
|
-
);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
497
|
-
// Root export
|
|
498
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
499
|
-
|
|
500
|
-
export function TreeContextMenu({ item, children }: TreeContextMenuProps) {
|
|
501
|
-
const selectedIds = useStructureStore((s) => s.selectedIds);
|
|
502
|
-
const selectItem = useStructureStore((s) => s.selectItem);
|
|
503
|
-
|
|
504
|
-
function handleOpenChange(open: boolean) {
|
|
505
|
-
if (!open) return;
|
|
506
|
-
// On right-click: if item is not in the current selection, select it alone
|
|
507
|
-
if (!selectedIds.has(item.id)) {
|
|
508
|
-
selectItem(item.id, item.type);
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
return (
|
|
513
|
-
<ContextMenu onOpenChange={handleOpenChange}>
|
|
514
|
-
<ContextMenuTrigger className="block w-full h-full">
|
|
515
|
-
{children}
|
|
516
|
-
</ContextMenuTrigger>
|
|
517
|
-
|
|
518
|
-
<ContextMenuContent className="w-56">
|
|
519
|
-
{item.type === 'course' && <CourseMenu data={item.data as Course} />}
|
|
520
|
-
{item.type === 'session' && <SessionMenu data={item.data as Session} />}
|
|
521
|
-
{item.type === 'lesson' && <LessonMenu data={item.data as Lesson} />}
|
|
522
|
-
</ContextMenuContent>
|
|
523
|
-
</ContextMenu>
|
|
524
|
-
);
|
|
525
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ContextMenu,
|
|
5
|
+
ContextMenuContent,
|
|
6
|
+
ContextMenuItem,
|
|
7
|
+
ContextMenuLabel,
|
|
8
|
+
ContextMenuSeparator,
|
|
9
|
+
ContextMenuTrigger,
|
|
10
|
+
} from '@/components/ui/context-menu';
|
|
11
|
+
import {
|
|
12
|
+
ArrowDown,
|
|
13
|
+
ArrowUp,
|
|
14
|
+
BookOpen,
|
|
15
|
+
Clipboard,
|
|
16
|
+
Copy,
|
|
17
|
+
FolderOpen,
|
|
18
|
+
Layers,
|
|
19
|
+
Loader2,
|
|
20
|
+
Pencil,
|
|
21
|
+
Plus,
|
|
22
|
+
Trash2,
|
|
23
|
+
} from 'lucide-react';
|
|
24
|
+
import { toast } from 'sonner';
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
useBulkDeleteMutation,
|
|
28
|
+
useCreateLessonMutation,
|
|
29
|
+
useCreateSessionMutation,
|
|
30
|
+
useDeleteLessonMutation,
|
|
31
|
+
useDeleteSessionMutation,
|
|
32
|
+
useDuplicateLessonMutation,
|
|
33
|
+
useDuplicateSessionMutation,
|
|
34
|
+
useMoveLessonsMutation,
|
|
35
|
+
usePasteLessonsMutation,
|
|
36
|
+
usePasteSessionsMutation,
|
|
37
|
+
useReorderSessionsMutation,
|
|
38
|
+
} from '../_data/use-course-structure-mutations';
|
|
39
|
+
import { useStructureStore } from './store';
|
|
40
|
+
import type { Course, FlatItem, Lesson, Session } from './types';
|
|
41
|
+
|
|
42
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
43
|
+
// Types
|
|
44
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
interface TreeContextMenuProps {
|
|
47
|
+
item: FlatItem;
|
|
48
|
+
children: React.ReactNode;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
52
|
+
// Sub-menus by item type
|
|
53
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
function CourseMenu({ data }: { data: Course }) {
|
|
56
|
+
const expandAll = useStructureStore((s) => s.expandAll);
|
|
57
|
+
const collapseAll = useStructureStore((s) => s.collapseAll);
|
|
58
|
+
const copiedType = useStructureStore((s) => s.copiedType);
|
|
59
|
+
const copiedIds = useStructureStore((s) => s.copiedIds);
|
|
60
|
+
|
|
61
|
+
const createSessionMutation = useCreateSessionMutation();
|
|
62
|
+
const pasteSessionsMutation = usePasteSessionsMutation();
|
|
63
|
+
const hasSessionClipboard = copiedType === 'session' && copiedIds.length > 0;
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<>
|
|
67
|
+
<ContextMenuLabel className="text-[0.65rem] text-muted-foreground px-2 py-1">
|
|
68
|
+
{data.title}
|
|
69
|
+
</ContextMenuLabel>
|
|
70
|
+
<ContextMenuSeparator />
|
|
71
|
+
|
|
72
|
+
<ContextMenuItem
|
|
73
|
+
disabled={createSessionMutation.isPending}
|
|
74
|
+
onSelect={() => {
|
|
75
|
+
createSessionMutation.mutate();
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
{createSessionMutation.isPending ? (
|
|
79
|
+
<Loader2 className="size-3.5 mr-2 animate-spin" />
|
|
80
|
+
) : (
|
|
81
|
+
<Plus className="size-3.5 mr-2 text-muted-foreground" />
|
|
82
|
+
)}
|
|
83
|
+
Nova sessão
|
|
84
|
+
</ContextMenuItem>
|
|
85
|
+
|
|
86
|
+
<ContextMenuSeparator />
|
|
87
|
+
|
|
88
|
+
<ContextMenuItem
|
|
89
|
+
onSelect={() => {
|
|
90
|
+
expandAll();
|
|
91
|
+
toast('Tudo expandido');
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
<BookOpen className="size-3.5 mr-2 text-muted-foreground" />
|
|
95
|
+
Expandir tudo
|
|
96
|
+
</ContextMenuItem>
|
|
97
|
+
|
|
98
|
+
<ContextMenuItem
|
|
99
|
+
onSelect={() => {
|
|
100
|
+
collapseAll();
|
|
101
|
+
toast('Tudo recolhido');
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
<Layers className="size-3.5 mr-2 text-muted-foreground" />
|
|
105
|
+
Recolher tudo
|
|
106
|
+
</ContextMenuItem>
|
|
107
|
+
|
|
108
|
+
{hasSessionClipboard && (
|
|
109
|
+
<>
|
|
110
|
+
<ContextMenuSeparator />
|
|
111
|
+
<ContextMenuItem
|
|
112
|
+
disabled={pasteSessionsMutation.isPending}
|
|
113
|
+
onSelect={() => {
|
|
114
|
+
pasteSessionsMutation.mutate({ sessionIds: copiedIds });
|
|
115
|
+
}}
|
|
116
|
+
>
|
|
117
|
+
{pasteSessionsMutation.isPending ? (
|
|
118
|
+
<Loader2 className="size-3.5 mr-2 animate-spin" />
|
|
119
|
+
) : (
|
|
120
|
+
<Clipboard className="size-3.5 mr-2 text-muted-foreground" />
|
|
121
|
+
)}
|
|
122
|
+
Colar sessão{copiedIds.length > 1 ? ` (${copiedIds.length})` : ''}
|
|
123
|
+
</ContextMenuItem>
|
|
124
|
+
</>
|
|
125
|
+
)}
|
|
126
|
+
</>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function SessionMenu({ data }: { data: Session }) {
|
|
131
|
+
const sessions = useStructureStore((s) => s.sessions);
|
|
132
|
+
const selectedIds = useStructureStore((s) => s.selectedIds);
|
|
133
|
+
const copiedType = useStructureStore((s) => s.copiedType);
|
|
134
|
+
const copiedIds = useStructureStore((s) => s.copiedIds);
|
|
135
|
+
const copyItems = useStructureStore((s) => s.copyItems);
|
|
136
|
+
const showConfirm = useStructureStore((s) => s.showConfirm);
|
|
137
|
+
const startRename = useStructureStore((s) => s.startRename);
|
|
138
|
+
|
|
139
|
+
const createLessonMutation = useCreateLessonMutation();
|
|
140
|
+
const deleteSessionMutation = useDeleteSessionMutation();
|
|
141
|
+
const duplicateSessionMutation = useDuplicateSessionMutation();
|
|
142
|
+
const pasteLessonsMutation = usePasteLessonsMutation();
|
|
143
|
+
const reorderSessionsMutation = useReorderSessionsMutation();
|
|
144
|
+
const reorderSessionsInStore = useStructureStore((s) => s.reorderSessions);
|
|
145
|
+
|
|
146
|
+
const sorted = [...sessions].sort((a, b) => a.order - b.order);
|
|
147
|
+
const idx = sorted.findIndex((s) => s.id === data.id);
|
|
148
|
+
const isFirst = idx === 0;
|
|
149
|
+
const isLast = idx === sorted.length - 1;
|
|
150
|
+
|
|
151
|
+
// Count selected sessions
|
|
152
|
+
const selectedSessionIds = sessions
|
|
153
|
+
.filter((s) => selectedIds.has(`session:${s.id}`))
|
|
154
|
+
.map((s) => s.id);
|
|
155
|
+
const multiSessions = selectedSessionIds.length > 1;
|
|
156
|
+
|
|
157
|
+
const hasLessonClipboard = copiedType === 'lesson' && copiedIds.length > 0;
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<>
|
|
161
|
+
<ContextMenuLabel className="text-[0.65rem] text-muted-foreground px-2 py-1">
|
|
162
|
+
{data.title}
|
|
163
|
+
</ContextMenuLabel>
|
|
164
|
+
<ContextMenuSeparator />
|
|
165
|
+
|
|
166
|
+
<ContextMenuItem
|
|
167
|
+
disabled={createLessonMutation.isPending}
|
|
168
|
+
onSelect={() => {
|
|
169
|
+
createLessonMutation.mutate({ sessionId: data.id });
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
{createLessonMutation.isPending ? (
|
|
173
|
+
<Loader2 className="size-3.5 mr-2 animate-spin" />
|
|
174
|
+
) : (
|
|
175
|
+
<Plus className="size-3.5 mr-2 text-muted-foreground" />
|
|
176
|
+
)}
|
|
177
|
+
Nova aula nesta sessão
|
|
178
|
+
</ContextMenuItem>
|
|
179
|
+
|
|
180
|
+
<ContextMenuSeparator />
|
|
181
|
+
|
|
182
|
+
<ContextMenuItem onSelect={() => startRename(data.id)}>
|
|
183
|
+
<Pencil className="size-3.5 mr-2 text-muted-foreground" />
|
|
184
|
+
Renomear
|
|
185
|
+
</ContextMenuItem>
|
|
186
|
+
|
|
187
|
+
<ContextMenuItem
|
|
188
|
+
disabled={duplicateSessionMutation.isPending}
|
|
189
|
+
onSelect={() => {
|
|
190
|
+
duplicateSessionMutation.mutate({ sessionId: data.id });
|
|
191
|
+
}}
|
|
192
|
+
>
|
|
193
|
+
{duplicateSessionMutation.isPending ? (
|
|
194
|
+
<Loader2 className="size-3.5 mr-2 animate-spin" />
|
|
195
|
+
) : (
|
|
196
|
+
<Copy className="size-3.5 mr-2 text-muted-foreground" />
|
|
197
|
+
)}
|
|
198
|
+
Duplicar sessão
|
|
199
|
+
</ContextMenuItem>
|
|
200
|
+
|
|
201
|
+
<ContextMenuItem
|
|
202
|
+
onSelect={() => {
|
|
203
|
+
copyItems([data.id], 'session');
|
|
204
|
+
toast.success('Sessão copiada');
|
|
205
|
+
}}
|
|
206
|
+
>
|
|
207
|
+
<Copy className="size-3.5 mr-2 text-muted-foreground" />
|
|
208
|
+
Copiar sessão
|
|
209
|
+
</ContextMenuItem>
|
|
210
|
+
|
|
211
|
+
{multiSessions && (
|
|
212
|
+
<ContextMenuItem
|
|
213
|
+
onSelect={() => {
|
|
214
|
+
copyItems(selectedSessionIds, 'session');
|
|
215
|
+
toast.success(`${selectedSessionIds.length} sessões copiadas`);
|
|
216
|
+
}}
|
|
217
|
+
>
|
|
218
|
+
<Copy className="size-3.5 mr-2 text-muted-foreground" />
|
|
219
|
+
Copiar selecionadas ({selectedSessionIds.length})
|
|
220
|
+
</ContextMenuItem>
|
|
221
|
+
)}
|
|
222
|
+
|
|
223
|
+
{hasLessonClipboard && (
|
|
224
|
+
<ContextMenuItem
|
|
225
|
+
disabled={pasteLessonsMutation.isPending}
|
|
226
|
+
onSelect={() => {
|
|
227
|
+
pasteLessonsMutation.mutate({
|
|
228
|
+
targetSessionId: data.id,
|
|
229
|
+
lessonIds: copiedIds,
|
|
230
|
+
});
|
|
231
|
+
}}
|
|
232
|
+
>
|
|
233
|
+
{pasteLessonsMutation.isPending ? (
|
|
234
|
+
<Loader2 className="size-3.5 mr-2 animate-spin" />
|
|
235
|
+
) : (
|
|
236
|
+
<Clipboard className="size-3.5 mr-2 text-muted-foreground" />
|
|
237
|
+
)}
|
|
238
|
+
Colar aulas nesta sessão
|
|
239
|
+
{copiedIds.length > 1 ? ` (${copiedIds.length})` : ''}
|
|
240
|
+
</ContextMenuItem>
|
|
241
|
+
)}
|
|
242
|
+
|
|
243
|
+
<ContextMenuSeparator />
|
|
244
|
+
|
|
245
|
+
<ContextMenuItem
|
|
246
|
+
disabled={isFirst || reorderSessionsMutation.isPending}
|
|
247
|
+
onSelect={() => {
|
|
248
|
+
const previousSessions = [...sorted];
|
|
249
|
+
reorderSessionsInStore(idx, idx - 1);
|
|
250
|
+
const newSorted = [...sorted];
|
|
251
|
+
const [moved] = newSorted.splice(idx, 1);
|
|
252
|
+
newSorted.splice(idx - 1, 0, moved!);
|
|
253
|
+
reorderSessionsMutation.mutate({
|
|
254
|
+
orderedIds: newSorted.map((s) => s.id),
|
|
255
|
+
previousSessions,
|
|
256
|
+
});
|
|
257
|
+
}}
|
|
258
|
+
>
|
|
259
|
+
<ArrowUp className="size-3.5 mr-2 text-muted-foreground" />
|
|
260
|
+
Mover para cima
|
|
261
|
+
</ContextMenuItem>
|
|
262
|
+
|
|
263
|
+
<ContextMenuItem
|
|
264
|
+
disabled={isLast || reorderSessionsMutation.isPending}
|
|
265
|
+
onSelect={() => {
|
|
266
|
+
const previousSessions = [...sorted];
|
|
267
|
+
reorderSessionsInStore(idx, idx + 1);
|
|
268
|
+
const newSorted = [...sorted];
|
|
269
|
+
const [moved] = newSorted.splice(idx, 1);
|
|
270
|
+
newSorted.splice(idx + 1, 0, moved!);
|
|
271
|
+
reorderSessionsMutation.mutate({
|
|
272
|
+
orderedIds: newSorted.map((s) => s.id),
|
|
273
|
+
previousSessions,
|
|
274
|
+
});
|
|
275
|
+
}}
|
|
276
|
+
>
|
|
277
|
+
<ArrowDown className="size-3.5 mr-2 text-muted-foreground" />
|
|
278
|
+
Mover para baixo
|
|
279
|
+
</ContextMenuItem>
|
|
280
|
+
|
|
281
|
+
<ContextMenuSeparator />
|
|
282
|
+
|
|
283
|
+
<ContextMenuItem
|
|
284
|
+
className="text-destructive focus:text-destructive"
|
|
285
|
+
onSelect={() => {
|
|
286
|
+
showConfirm({
|
|
287
|
+
title: `Excluir "${data.title}"?`,
|
|
288
|
+
description:
|
|
289
|
+
'A sessão e todas as suas aulas serão excluídas permanentemente.',
|
|
290
|
+
onConfirm: () => {
|
|
291
|
+
deleteSessionMutation.mutate({ sessionId: data.id });
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
}}
|
|
295
|
+
>
|
|
296
|
+
<Trash2 className="size-3.5 mr-2" />
|
|
297
|
+
Excluir sessão
|
|
298
|
+
</ContextMenuItem>
|
|
299
|
+
</>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function LessonMenu({ data }: { data: Lesson }) {
|
|
304
|
+
const lessons = useStructureStore((s) => s.lessons);
|
|
305
|
+
const sessions = useStructureStore((s) => s.sessions);
|
|
306
|
+
const selectedIds = useStructureStore((s) => s.selectedIds);
|
|
307
|
+
const copiedType = useStructureStore((s) => s.copiedType);
|
|
308
|
+
const copiedIds = useStructureStore((s) => s.copiedIds);
|
|
309
|
+
const copyItems = useStructureStore((s) => s.copyItems);
|
|
310
|
+
const showSessionPicker = useStructureStore((s) => s.showSessionPicker);
|
|
311
|
+
const showConfirm = useStructureStore((s) => s.showConfirm);
|
|
312
|
+
const startRename = useStructureStore((s) => s.startRename);
|
|
313
|
+
|
|
314
|
+
const deleteLessonMutation = useDeleteLessonMutation();
|
|
315
|
+
const bulkDeleteMutation = useBulkDeleteMutation();
|
|
316
|
+
const duplicateLessonMutation = useDuplicateLessonMutation();
|
|
317
|
+
const pasteLessonsMutation = usePasteLessonsMutation();
|
|
318
|
+
const moveLessonsMutation = useMoveLessonsMutation();
|
|
319
|
+
|
|
320
|
+
// Count selected lessons
|
|
321
|
+
const selectedLessonIds = lessons
|
|
322
|
+
.filter((l) => selectedIds.has(l.id))
|
|
323
|
+
.map((l) => l.id);
|
|
324
|
+
const multiLessons = selectedLessonIds.length > 1;
|
|
325
|
+
|
|
326
|
+
const hasLessonClipboard = copiedType === 'lesson' && copiedIds.length > 0;
|
|
327
|
+
const hasOtherSessions = sessions.some((ss) => ss.id !== data.sessionId);
|
|
328
|
+
|
|
329
|
+
// Ids to move when "Mover" is triggered
|
|
330
|
+
const idsToMove = multiLessons ? selectedLessonIds : [data.id];
|
|
331
|
+
|
|
332
|
+
return (
|
|
333
|
+
<>
|
|
334
|
+
<ContextMenuLabel className="text-[0.65rem] text-muted-foreground px-2 py-1">
|
|
335
|
+
{data.title}
|
|
336
|
+
</ContextMenuLabel>
|
|
337
|
+
<ContextMenuSeparator />
|
|
338
|
+
|
|
339
|
+
<ContextMenuItem onSelect={() => startRename(data.id)}>
|
|
340
|
+
<Pencil className="size-3.5 mr-2 text-muted-foreground" />
|
|
341
|
+
Renomear
|
|
342
|
+
</ContextMenuItem>
|
|
343
|
+
|
|
344
|
+
<ContextMenuSeparator />
|
|
345
|
+
|
|
346
|
+
<ContextMenuItem
|
|
347
|
+
disabled={duplicateLessonMutation.isPending}
|
|
348
|
+
onSelect={() => {
|
|
349
|
+
duplicateLessonMutation.mutate({
|
|
350
|
+
sessionId: data.sessionId,
|
|
351
|
+
lessonId: data.id,
|
|
352
|
+
});
|
|
353
|
+
}}
|
|
354
|
+
>
|
|
355
|
+
{duplicateLessonMutation.isPending ? (
|
|
356
|
+
<Loader2 className="size-3.5 mr-2 animate-spin" />
|
|
357
|
+
) : (
|
|
358
|
+
<Copy className="size-3.5 mr-2 text-muted-foreground" />
|
|
359
|
+
)}
|
|
360
|
+
Duplicar aula
|
|
361
|
+
</ContextMenuItem>
|
|
362
|
+
|
|
363
|
+
<ContextMenuItem
|
|
364
|
+
onSelect={() => {
|
|
365
|
+
copyItems([data.id], 'lesson');
|
|
366
|
+
toast.success('Aula copiada');
|
|
367
|
+
}}
|
|
368
|
+
>
|
|
369
|
+
<Copy className="size-3.5 mr-2 text-muted-foreground" />
|
|
370
|
+
Copiar aula
|
|
371
|
+
</ContextMenuItem>
|
|
372
|
+
|
|
373
|
+
{multiLessons && (
|
|
374
|
+
<ContextMenuItem
|
|
375
|
+
onSelect={() => {
|
|
376
|
+
copyItems(selectedLessonIds, 'lesson');
|
|
377
|
+
toast.success(`${selectedLessonIds.length} aulas copiadas`);
|
|
378
|
+
}}
|
|
379
|
+
>
|
|
380
|
+
<Copy className="size-3.5 mr-2 text-muted-foreground" />
|
|
381
|
+
Copiar selecionadas ({selectedLessonIds.length})
|
|
382
|
+
</ContextMenuItem>
|
|
383
|
+
)}
|
|
384
|
+
|
|
385
|
+
{hasLessonClipboard && (
|
|
386
|
+
<ContextMenuItem
|
|
387
|
+
disabled={pasteLessonsMutation.isPending}
|
|
388
|
+
onSelect={() => {
|
|
389
|
+
pasteLessonsMutation.mutate({
|
|
390
|
+
targetSessionId: data.sessionId,
|
|
391
|
+
lessonIds: copiedIds,
|
|
392
|
+
});
|
|
393
|
+
}}
|
|
394
|
+
>
|
|
395
|
+
{pasteLessonsMutation.isPending ? (
|
|
396
|
+
<Loader2 className="size-3.5 mr-2 animate-spin" />
|
|
397
|
+
) : (
|
|
398
|
+
<Clipboard className="size-3.5 mr-2 text-muted-foreground" />
|
|
399
|
+
)}
|
|
400
|
+
Colar nesta sessão
|
|
401
|
+
</ContextMenuItem>
|
|
402
|
+
)}
|
|
403
|
+
|
|
404
|
+
{hasOtherSessions && (
|
|
405
|
+
<>
|
|
406
|
+
<ContextMenuSeparator />
|
|
407
|
+
<ContextMenuItem
|
|
408
|
+
disabled={moveLessonsMutation.isPending}
|
|
409
|
+
onSelect={() => {
|
|
410
|
+
showSessionPicker({
|
|
411
|
+
title:
|
|
412
|
+
idsToMove.length > 1
|
|
413
|
+
? `Mover ${idsToMove.length} aulas para`
|
|
414
|
+
: 'Mover aula para',
|
|
415
|
+
excludeSessionId: data.sessionId,
|
|
416
|
+
onPick: (targetId) => {
|
|
417
|
+
// Snapshot for rollback
|
|
418
|
+
const previousLessons = [...lessons];
|
|
419
|
+
// Compute moves with sequential toIndex
|
|
420
|
+
const movingLessons = idsToMove
|
|
421
|
+
.map((id) => lessons.find((l) => l.id === id))
|
|
422
|
+
.filter(Boolean) as Lesson[];
|
|
423
|
+
const moves = movingLessons.map((l, i) => ({
|
|
424
|
+
lessonId: l.id,
|
|
425
|
+
fromSessionId: l.sessionId,
|
|
426
|
+
toSessionId: targetId,
|
|
427
|
+
toIndex: i,
|
|
428
|
+
}));
|
|
429
|
+
// Optimistic: update store immediately
|
|
430
|
+
useStructureStore.getState().moveLessons(idsToMove, targetId);
|
|
431
|
+
moveLessonsMutation.mutate({ moves, previousLessons });
|
|
432
|
+
toast.success(
|
|
433
|
+
idsToMove.length > 1
|
|
434
|
+
? `${idsToMove.length} aulas movidas`
|
|
435
|
+
: 'Aula movida'
|
|
436
|
+
);
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
}}
|
|
440
|
+
>
|
|
441
|
+
{moveLessonsMutation.isPending ? (
|
|
442
|
+
<Loader2 className="size-3.5 mr-2 animate-spin" />
|
|
443
|
+
) : (
|
|
444
|
+
<FolderOpen className="size-3.5 mr-2 text-muted-foreground" />
|
|
445
|
+
)}
|
|
446
|
+
{multiLessons
|
|
447
|
+
? `Mover ${idsToMove.length} aulas para...`
|
|
448
|
+
: 'Mover para outra sessão...'}
|
|
449
|
+
</ContextMenuItem>
|
|
450
|
+
</>
|
|
451
|
+
)}
|
|
452
|
+
|
|
453
|
+
<ContextMenuSeparator />
|
|
454
|
+
|
|
455
|
+
<ContextMenuItem
|
|
456
|
+
className="text-destructive focus:text-destructive"
|
|
457
|
+
onSelect={() => {
|
|
458
|
+
if (multiLessons) {
|
|
459
|
+
showConfirm({
|
|
460
|
+
title: `Excluir ${selectedLessonIds.length} aulas?`,
|
|
461
|
+
description: 'Esta ação não pode ser desfeita.',
|
|
462
|
+
onConfirm: () => {
|
|
463
|
+
const lessonPairs = selectedLessonIds
|
|
464
|
+
.map((id) => lessons.find((l) => l.id === id))
|
|
465
|
+
.filter(Boolean)
|
|
466
|
+
.map((l) => ({ lessonId: l!.id, sessionId: l!.sessionId }));
|
|
467
|
+
bulkDeleteMutation.mutate({
|
|
468
|
+
sessionIds: [],
|
|
469
|
+
lessons: lessonPairs,
|
|
470
|
+
});
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
} else {
|
|
474
|
+
showConfirm({
|
|
475
|
+
title: `Excluir "${data.title}"?`,
|
|
476
|
+
description: 'Esta ação não pode ser desfeita.',
|
|
477
|
+
onConfirm: () => {
|
|
478
|
+
deleteLessonMutation.mutate({
|
|
479
|
+
lessonId: data.id,
|
|
480
|
+
sessionId: data.sessionId,
|
|
481
|
+
});
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}}
|
|
486
|
+
>
|
|
487
|
+
<Trash2 className="size-3.5 mr-2" />
|
|
488
|
+
{multiLessons
|
|
489
|
+
? `Excluir ${selectedLessonIds.length} aulas`
|
|
490
|
+
: 'Excluir aula'}
|
|
491
|
+
</ContextMenuItem>
|
|
492
|
+
</>
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
497
|
+
// Root export
|
|
498
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
499
|
+
|
|
500
|
+
export function TreeContextMenu({ item, children }: TreeContextMenuProps) {
|
|
501
|
+
const selectedIds = useStructureStore((s) => s.selectedIds);
|
|
502
|
+
const selectItem = useStructureStore((s) => s.selectItem);
|
|
503
|
+
|
|
504
|
+
function handleOpenChange(open: boolean) {
|
|
505
|
+
if (!open) return;
|
|
506
|
+
// On right-click: if item is not in the current selection, select it alone
|
|
507
|
+
if (!selectedIds.has(item.id)) {
|
|
508
|
+
selectItem(item.id, item.type);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return (
|
|
513
|
+
<ContextMenu onOpenChange={handleOpenChange}>
|
|
514
|
+
<ContextMenuTrigger className="block w-full h-full">
|
|
515
|
+
{children}
|
|
516
|
+
</ContextMenuTrigger>
|
|
517
|
+
|
|
518
|
+
<ContextMenuContent className="w-56">
|
|
519
|
+
{item.type === 'course' && <CourseMenu data={item.data as Course} />}
|
|
520
|
+
{item.type === 'session' && <SessionMenu data={item.data as Session} />}
|
|
521
|
+
{item.type === 'lesson' && <LessonMenu data={item.data as Lesson} />}
|
|
522
|
+
</ContextMenuContent>
|
|
523
|
+
</ContextMenu>
|
|
524
|
+
);
|
|
525
|
+
}
|