@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
package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs
CHANGED
|
@@ -1,318 +1,318 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useCallback, useEffect, useRef } from 'react';
|
|
4
|
-
import { toast } from 'sonner';
|
|
5
|
-
|
|
6
|
-
import type { SearchFilterHandle } from './search-filter';
|
|
7
|
-
import { useStructureStore } from './store';
|
|
8
|
-
import { buildVisibleItems } from './tree-helpers';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Checks whether keyboard shortcut handling should be suppressed
|
|
12
|
-
* (the user is typing in a form control or rich-text editor).
|
|
13
|
-
*/
|
|
14
|
-
function isEditingTarget(el: Element | null): boolean {
|
|
15
|
-
if (!el) return false;
|
|
16
|
-
const tag = (el as HTMLElement).tagName.toLowerCase();
|
|
17
|
-
if (tag === 'input' || tag === 'textarea' || tag === 'select') return true;
|
|
18
|
-
// contenteditable (Tiptap, etc.)
|
|
19
|
-
if ((el as HTMLElement).isContentEditable) return true;
|
|
20
|
-
// shadcn Select portal, combobox, etc. — has [role=combobox] or [role=listbox]
|
|
21
|
-
const role = (el as HTMLElement).getAttribute('role');
|
|
22
|
-
if (role === 'combobox' || role === 'listbox' || role === 'option')
|
|
23
|
-
return true;
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
interface UseCourseStructureShortcutsOptions {
|
|
28
|
-
/** Ref to the SearchFilter imperative handle so Ctrl+F / Escape can focus/clear it. */
|
|
29
|
-
searchRef: React.RefObject<SearchFilterHandle | null>;
|
|
30
|
-
/** Ref to the detail panel wrapper div (for Enter key focusing). */
|
|
31
|
-
detailPanelRef: React.RefObject<HTMLDivElement | null>;
|
|
32
|
-
/** Callback to open/close the shortcuts help sheet. */
|
|
33
|
-
onToggleHelp: () => void;
|
|
34
|
-
/** API-backed duplicate (session or lesson based on active item). */
|
|
35
|
-
onDuplicate?: () => void;
|
|
36
|
-
/** API-backed paste (lesson or session clipboard). */
|
|
37
|
-
onPaste?: () => void;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function useCourseStructureShortcuts({
|
|
41
|
-
searchRef,
|
|
42
|
-
detailPanelRef,
|
|
43
|
-
onToggleHelp,
|
|
44
|
-
onDuplicate,
|
|
45
|
-
onPaste,
|
|
46
|
-
}: UseCourseStructureShortcutsOptions) {
|
|
47
|
-
const course = useStructureStore((s) => s.course);
|
|
48
|
-
const sessions = useStructureStore((s) => s.sessions);
|
|
49
|
-
const lessons = useStructureStore((s) => s.lessons);
|
|
50
|
-
const expandedIds = useStructureStore((s) => s.expandedIds);
|
|
51
|
-
const filterQuery = useStructureStore((s) => s.filterQuery);
|
|
52
|
-
const selectedIds = useStructureStore((s) => s.selectedIds);
|
|
53
|
-
|
|
54
|
-
const activeItemId = useStructureStore((s) => s.activeItemId);
|
|
55
|
-
const activeItemType = useStructureStore((s) => s.activeItemType);
|
|
56
|
-
|
|
57
|
-
const selectItem = useStructureStore((s) => s.selectItem);
|
|
58
|
-
const clearSelection = useStructureStore((s) => s.clearSelection);
|
|
59
|
-
const setFilter = useStructureStore((s) => s.setFilter);
|
|
60
|
-
const toggleExpand = useStructureStore((s) => s.toggleExpand);
|
|
61
|
-
const copyItems = useStructureStore((s) => s.copyItems);
|
|
62
|
-
const addSession = useStructureStore((s) => s.addSession);
|
|
63
|
-
const addLesson = useStructureStore((s) => s.addLesson);
|
|
64
|
-
const deleteSession = useStructureStore((s) => s.deleteSession);
|
|
65
|
-
const deleteLesson = useStructureStore((s) => s.deleteLesson);
|
|
66
|
-
const deleteSelected = useStructureStore((s) => s.deleteSelected);
|
|
67
|
-
const duplicateSelected = useStructureStore((s) => s.duplicateSelected);
|
|
68
|
-
const showConfirm = useStructureStore((s) => s.showConfirm);
|
|
69
|
-
|
|
70
|
-
// Keep a stable ref to store values so handlers don't stale-close
|
|
71
|
-
const stateRef = useRef({
|
|
72
|
-
course,
|
|
73
|
-
sessions,
|
|
74
|
-
lessons,
|
|
75
|
-
expandedIds,
|
|
76
|
-
filterQuery,
|
|
77
|
-
selectedIds,
|
|
78
|
-
activeItemId,
|
|
79
|
-
activeItemType,
|
|
80
|
-
selectItem,
|
|
81
|
-
clearSelection,
|
|
82
|
-
setFilter,
|
|
83
|
-
toggleExpand,
|
|
84
|
-
copyItems,
|
|
85
|
-
addSession,
|
|
86
|
-
addLesson,
|
|
87
|
-
deleteSession,
|
|
88
|
-
deleteLesson,
|
|
89
|
-
deleteSelected,
|
|
90
|
-
duplicateSelected,
|
|
91
|
-
showConfirm,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
useEffect(() => {
|
|
95
|
-
stateRef.current = {
|
|
96
|
-
course,
|
|
97
|
-
sessions,
|
|
98
|
-
lessons,
|
|
99
|
-
expandedIds,
|
|
100
|
-
filterQuery,
|
|
101
|
-
selectedIds,
|
|
102
|
-
activeItemId,
|
|
103
|
-
activeItemType,
|
|
104
|
-
selectItem,
|
|
105
|
-
clearSelection,
|
|
106
|
-
setFilter,
|
|
107
|
-
toggleExpand,
|
|
108
|
-
copyItems,
|
|
109
|
-
addSession,
|
|
110
|
-
addLesson,
|
|
111
|
-
deleteSession,
|
|
112
|
-
deleteLesson,
|
|
113
|
-
deleteSelected,
|
|
114
|
-
duplicateSelected,
|
|
115
|
-
showConfirm,
|
|
116
|
-
};
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
const handleKeyDown = useCallback(
|
|
120
|
-
(e: KeyboardEvent) => {
|
|
121
|
-
const target = document.activeElement;
|
|
122
|
-
const ctrl = e.ctrlKey || e.metaKey;
|
|
123
|
-
|
|
124
|
-
// ── Ctrl/Cmd + ? always work (help, search) ──────────────────────────
|
|
125
|
-
if (ctrl && e.key === '/') {
|
|
126
|
-
e.preventDefault();
|
|
127
|
-
onToggleHelp();
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (ctrl && e.key.toLowerCase() === 'f') {
|
|
132
|
-
e.preventDefault();
|
|
133
|
-
searchRef.current?.focus();
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// ── Ctrl/Cmd + S — save active detail panel form ──────────────────────
|
|
138
|
-
if (ctrl && e.key.toLowerCase() === 's') {
|
|
139
|
-
e.preventDefault();
|
|
140
|
-
if (detailPanelRef.current) {
|
|
141
|
-
const submitBtn =
|
|
142
|
-
detailPanelRef.current.querySelector<HTMLButtonElement>(
|
|
143
|
-
'button[type="submit"]'
|
|
144
|
-
);
|
|
145
|
-
submitBtn?.click();
|
|
146
|
-
}
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// ── Suppress all other shortcuts when editing ─────────────────────────
|
|
151
|
-
if (isEditingTarget(target)) return;
|
|
152
|
-
|
|
153
|
-
const s = stateRef.current;
|
|
154
|
-
|
|
155
|
-
// Build the current visible list (same logic as the tree)
|
|
156
|
-
const { items } = buildVisibleItems(
|
|
157
|
-
s.course,
|
|
158
|
-
s.sessions,
|
|
159
|
-
s.lessons,
|
|
160
|
-
s.expandedIds,
|
|
161
|
-
s.filterQuery
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
const currentIdx = items.findIndex((i) => i.id === s.activeItemId);
|
|
165
|
-
const current = currentIdx !== -1 ? items[currentIdx] : null;
|
|
166
|
-
|
|
167
|
-
// ── Arrow navigation ──────────────────────────────────────────────────
|
|
168
|
-
if (e.key === 'ArrowDown') {
|
|
169
|
-
e.preventDefault();
|
|
170
|
-
const next = items[currentIdx + 1];
|
|
171
|
-
if (next) s.selectItem(next.id, next.type, {}, items);
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (e.key === 'ArrowUp') {
|
|
176
|
-
e.preventDefault();
|
|
177
|
-
const prev = items[currentIdx - 1];
|
|
178
|
-
if (prev) s.selectItem(prev.id, prev.type, {}, items);
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (e.key === 'ArrowRight') {
|
|
183
|
-
e.preventDefault();
|
|
184
|
-
if (current?.type === 'session' && !s.expandedIds.has(current.id)) {
|
|
185
|
-
s.toggleExpand(current.id);
|
|
186
|
-
}
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (e.key === 'ArrowLeft') {
|
|
191
|
-
e.preventDefault();
|
|
192
|
-
if (current?.type === 'session' && s.expandedIds.has(current.id)) {
|
|
193
|
-
s.toggleExpand(current.id);
|
|
194
|
-
} else if (current?.type === 'lesson') {
|
|
195
|
-
// Go to parent session
|
|
196
|
-
const parent = items.find(
|
|
197
|
-
(i) => i.type === 'session' && i.id === current.data.sessionId
|
|
198
|
-
);
|
|
199
|
-
if (parent) s.selectItem(parent.id, 'session', {}, items);
|
|
200
|
-
}
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// ── Enter — focus first detail field ─────────────────────────────────
|
|
205
|
-
if (e.key === 'Enter') {
|
|
206
|
-
e.preventDefault();
|
|
207
|
-
if (detailPanelRef.current) {
|
|
208
|
-
const firstInput = detailPanelRef.current.querySelector<HTMLElement>(
|
|
209
|
-
'input:not([disabled]),textarea:not([disabled])'
|
|
210
|
-
);
|
|
211
|
-
firstInput?.focus();
|
|
212
|
-
}
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// ── Escape ────────────────────────────────────────────────────────────
|
|
217
|
-
if (e.key === 'Escape') {
|
|
218
|
-
e.preventDefault();
|
|
219
|
-
if (s.filterQuery) {
|
|
220
|
-
s.setFilter('');
|
|
221
|
-
searchRef.current?.clear();
|
|
222
|
-
} else if (s.selectedIds.size > 1) {
|
|
223
|
-
s.clearSelection();
|
|
224
|
-
} else {
|
|
225
|
-
(document.activeElement as HTMLElement)?.blur();
|
|
226
|
-
}
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// ── Delete ────────────────────────────────────────────────────────────
|
|
231
|
-
if (e.key === 'Delete') {
|
|
232
|
-
e.preventDefault();
|
|
233
|
-
const ids = [...s.selectedIds].filter(
|
|
234
|
-
(key) => key !== `course:${s.course.id}`
|
|
235
|
-
);
|
|
236
|
-
if (!ids.length) return;
|
|
237
|
-
const count = ids.length;
|
|
238
|
-
s.showConfirm({
|
|
239
|
-
title: `Excluir ${count > 1 ? count + ' itens' : 'item selecionado'}?`,
|
|
240
|
-
description: 'Esta ação não pode ser desfeita.',
|
|
241
|
-
onConfirm: () => {
|
|
242
|
-
s.deleteSelected();
|
|
243
|
-
toast.success(
|
|
244
|
-
count > 1 ? `${count} itens excluídos` : 'Item excluído'
|
|
245
|
-
);
|
|
246
|
-
},
|
|
247
|
-
});
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// ── Ctrl/Cmd + C ─────────────────────────────────────────────────────
|
|
252
|
-
if (ctrl && e.key.toLowerCase() === 'c') {
|
|
253
|
-
e.preventDefault();
|
|
254
|
-
const selectedKeys = [...s.selectedIds].filter(
|
|
255
|
-
(key) => key.startsWith('lesson:') || key.startsWith('session:')
|
|
256
|
-
);
|
|
257
|
-
if (!selectedKeys.length) return;
|
|
258
|
-
const firstKey = selectedKeys[0]!;
|
|
259
|
-
const type = firstKey.startsWith('lesson:') ? 'lesson' : 'session';
|
|
260
|
-
const ids = selectedKeys.map((key) => key.slice(key.indexOf(':') + 1));
|
|
261
|
-
s.copyItems(ids, type);
|
|
262
|
-
toast.success(
|
|
263
|
-
`${ids.length > 1 ? ids.length + ' itens copiados' : 'Item copiado'}`
|
|
264
|
-
);
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// ── Ctrl/Cmd + D — duplicate ──────────────────────────────────────────
|
|
269
|
-
if (ctrl && e.key.toLowerCase() === 'd') {
|
|
270
|
-
e.preventDefault();
|
|
271
|
-
if (onDuplicate) {
|
|
272
|
-
onDuplicate();
|
|
273
|
-
} else {
|
|
274
|
-
s.duplicateSelected();
|
|
275
|
-
toast.success('Item duplicado');
|
|
276
|
-
}
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// ── Ctrl/Cmd + V — paste ──────────────────────────────────────────────
|
|
281
|
-
if (ctrl && e.key.toLowerCase() === 'v') {
|
|
282
|
-
e.preventDefault();
|
|
283
|
-
if (onPaste) {
|
|
284
|
-
onPaste();
|
|
285
|
-
} else {
|
|
286
|
-
toast('Nada para colar');
|
|
287
|
-
}
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// ── Ctrl/Cmd + N — new item ───────────────────────────────────────────
|
|
292
|
-
if (ctrl && e.key.toLowerCase() === 'n') {
|
|
293
|
-
e.preventDefault();
|
|
294
|
-
if (s.activeItemType === 'course') {
|
|
295
|
-
s.addSession();
|
|
296
|
-
toast.success('Nova sessão criada');
|
|
297
|
-
} else if (s.activeItemType === 'session' && s.activeItemId) {
|
|
298
|
-
s.addLesson(s.activeItemId);
|
|
299
|
-
toast.success('Nova aula criada');
|
|
300
|
-
} else if (s.activeItemType === 'lesson') {
|
|
301
|
-
// Find parent session and add there
|
|
302
|
-
const lesson = s.lessons.find((l) => l.id === s.activeItemId);
|
|
303
|
-
if (lesson) {
|
|
304
|
-
s.addLesson(lesson.sessionId);
|
|
305
|
-
toast.success('Nova aula criada na mesma sessão');
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
},
|
|
311
|
-
[onToggleHelp, searchRef, detailPanelRef, onDuplicate, onPaste]
|
|
312
|
-
);
|
|
313
|
-
|
|
314
|
-
useEffect(() => {
|
|
315
|
-
window.addEventListener('keydown', handleKeyDown);
|
|
316
|
-
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
317
|
-
}, [handleKeyDown]);
|
|
318
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
4
|
+
import { toast } from 'sonner';
|
|
5
|
+
|
|
6
|
+
import type { SearchFilterHandle } from './search-filter';
|
|
7
|
+
import { useStructureStore } from './store';
|
|
8
|
+
import { buildVisibleItems } from './tree-helpers';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Checks whether keyboard shortcut handling should be suppressed
|
|
12
|
+
* (the user is typing in a form control or rich-text editor).
|
|
13
|
+
*/
|
|
14
|
+
function isEditingTarget(el: Element | null): boolean {
|
|
15
|
+
if (!el) return false;
|
|
16
|
+
const tag = (el as HTMLElement).tagName.toLowerCase();
|
|
17
|
+
if (tag === 'input' || tag === 'textarea' || tag === 'select') return true;
|
|
18
|
+
// contenteditable (Tiptap, etc.)
|
|
19
|
+
if ((el as HTMLElement).isContentEditable) return true;
|
|
20
|
+
// shadcn Select portal, combobox, etc. — has [role=combobox] or [role=listbox]
|
|
21
|
+
const role = (el as HTMLElement).getAttribute('role');
|
|
22
|
+
if (role === 'combobox' || role === 'listbox' || role === 'option')
|
|
23
|
+
return true;
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface UseCourseStructureShortcutsOptions {
|
|
28
|
+
/** Ref to the SearchFilter imperative handle so Ctrl+F / Escape can focus/clear it. */
|
|
29
|
+
searchRef: React.RefObject<SearchFilterHandle | null>;
|
|
30
|
+
/** Ref to the detail panel wrapper div (for Enter key focusing). */
|
|
31
|
+
detailPanelRef: React.RefObject<HTMLDivElement | null>;
|
|
32
|
+
/** Callback to open/close the shortcuts help sheet. */
|
|
33
|
+
onToggleHelp: () => void;
|
|
34
|
+
/** API-backed duplicate (session or lesson based on active item). */
|
|
35
|
+
onDuplicate?: () => void;
|
|
36
|
+
/** API-backed paste (lesson or session clipboard). */
|
|
37
|
+
onPaste?: () => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function useCourseStructureShortcuts({
|
|
41
|
+
searchRef,
|
|
42
|
+
detailPanelRef,
|
|
43
|
+
onToggleHelp,
|
|
44
|
+
onDuplicate,
|
|
45
|
+
onPaste,
|
|
46
|
+
}: UseCourseStructureShortcutsOptions) {
|
|
47
|
+
const course = useStructureStore((s) => s.course);
|
|
48
|
+
const sessions = useStructureStore((s) => s.sessions);
|
|
49
|
+
const lessons = useStructureStore((s) => s.lessons);
|
|
50
|
+
const expandedIds = useStructureStore((s) => s.expandedIds);
|
|
51
|
+
const filterQuery = useStructureStore((s) => s.filterQuery);
|
|
52
|
+
const selectedIds = useStructureStore((s) => s.selectedIds);
|
|
53
|
+
|
|
54
|
+
const activeItemId = useStructureStore((s) => s.activeItemId);
|
|
55
|
+
const activeItemType = useStructureStore((s) => s.activeItemType);
|
|
56
|
+
|
|
57
|
+
const selectItem = useStructureStore((s) => s.selectItem);
|
|
58
|
+
const clearSelection = useStructureStore((s) => s.clearSelection);
|
|
59
|
+
const setFilter = useStructureStore((s) => s.setFilter);
|
|
60
|
+
const toggleExpand = useStructureStore((s) => s.toggleExpand);
|
|
61
|
+
const copyItems = useStructureStore((s) => s.copyItems);
|
|
62
|
+
const addSession = useStructureStore((s) => s.addSession);
|
|
63
|
+
const addLesson = useStructureStore((s) => s.addLesson);
|
|
64
|
+
const deleteSession = useStructureStore((s) => s.deleteSession);
|
|
65
|
+
const deleteLesson = useStructureStore((s) => s.deleteLesson);
|
|
66
|
+
const deleteSelected = useStructureStore((s) => s.deleteSelected);
|
|
67
|
+
const duplicateSelected = useStructureStore((s) => s.duplicateSelected);
|
|
68
|
+
const showConfirm = useStructureStore((s) => s.showConfirm);
|
|
69
|
+
|
|
70
|
+
// Keep a stable ref to store values so handlers don't stale-close
|
|
71
|
+
const stateRef = useRef({
|
|
72
|
+
course,
|
|
73
|
+
sessions,
|
|
74
|
+
lessons,
|
|
75
|
+
expandedIds,
|
|
76
|
+
filterQuery,
|
|
77
|
+
selectedIds,
|
|
78
|
+
activeItemId,
|
|
79
|
+
activeItemType,
|
|
80
|
+
selectItem,
|
|
81
|
+
clearSelection,
|
|
82
|
+
setFilter,
|
|
83
|
+
toggleExpand,
|
|
84
|
+
copyItems,
|
|
85
|
+
addSession,
|
|
86
|
+
addLesson,
|
|
87
|
+
deleteSession,
|
|
88
|
+
deleteLesson,
|
|
89
|
+
deleteSelected,
|
|
90
|
+
duplicateSelected,
|
|
91
|
+
showConfirm,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
stateRef.current = {
|
|
96
|
+
course,
|
|
97
|
+
sessions,
|
|
98
|
+
lessons,
|
|
99
|
+
expandedIds,
|
|
100
|
+
filterQuery,
|
|
101
|
+
selectedIds,
|
|
102
|
+
activeItemId,
|
|
103
|
+
activeItemType,
|
|
104
|
+
selectItem,
|
|
105
|
+
clearSelection,
|
|
106
|
+
setFilter,
|
|
107
|
+
toggleExpand,
|
|
108
|
+
copyItems,
|
|
109
|
+
addSession,
|
|
110
|
+
addLesson,
|
|
111
|
+
deleteSession,
|
|
112
|
+
deleteLesson,
|
|
113
|
+
deleteSelected,
|
|
114
|
+
duplicateSelected,
|
|
115
|
+
showConfirm,
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const handleKeyDown = useCallback(
|
|
120
|
+
(e: KeyboardEvent) => {
|
|
121
|
+
const target = document.activeElement;
|
|
122
|
+
const ctrl = e.ctrlKey || e.metaKey;
|
|
123
|
+
|
|
124
|
+
// ── Ctrl/Cmd + ? always work (help, search) ──────────────────────────
|
|
125
|
+
if (ctrl && e.key === '/') {
|
|
126
|
+
e.preventDefault();
|
|
127
|
+
onToggleHelp();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (ctrl && e.key.toLowerCase() === 'f') {
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
searchRef.current?.focus();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── Ctrl/Cmd + S — save active detail panel form ──────────────────────
|
|
138
|
+
if (ctrl && e.key.toLowerCase() === 's') {
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
if (detailPanelRef.current) {
|
|
141
|
+
const submitBtn =
|
|
142
|
+
detailPanelRef.current.querySelector<HTMLButtonElement>(
|
|
143
|
+
'button[type="submit"]'
|
|
144
|
+
);
|
|
145
|
+
submitBtn?.click();
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Suppress all other shortcuts when editing ─────────────────────────
|
|
151
|
+
if (isEditingTarget(target)) return;
|
|
152
|
+
|
|
153
|
+
const s = stateRef.current;
|
|
154
|
+
|
|
155
|
+
// Build the current visible list (same logic as the tree)
|
|
156
|
+
const { items } = buildVisibleItems(
|
|
157
|
+
s.course,
|
|
158
|
+
s.sessions,
|
|
159
|
+
s.lessons,
|
|
160
|
+
s.expandedIds,
|
|
161
|
+
s.filterQuery
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const currentIdx = items.findIndex((i) => i.id === s.activeItemId);
|
|
165
|
+
const current = currentIdx !== -1 ? items[currentIdx] : null;
|
|
166
|
+
|
|
167
|
+
// ── Arrow navigation ──────────────────────────────────────────────────
|
|
168
|
+
if (e.key === 'ArrowDown') {
|
|
169
|
+
e.preventDefault();
|
|
170
|
+
const next = items[currentIdx + 1];
|
|
171
|
+
if (next) s.selectItem(next.id, next.type, {}, items);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (e.key === 'ArrowUp') {
|
|
176
|
+
e.preventDefault();
|
|
177
|
+
const prev = items[currentIdx - 1];
|
|
178
|
+
if (prev) s.selectItem(prev.id, prev.type, {}, items);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (e.key === 'ArrowRight') {
|
|
183
|
+
e.preventDefault();
|
|
184
|
+
if (current?.type === 'session' && !s.expandedIds.has(current.id)) {
|
|
185
|
+
s.toggleExpand(current.id);
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (e.key === 'ArrowLeft') {
|
|
191
|
+
e.preventDefault();
|
|
192
|
+
if (current?.type === 'session' && s.expandedIds.has(current.id)) {
|
|
193
|
+
s.toggleExpand(current.id);
|
|
194
|
+
} else if (current?.type === 'lesson') {
|
|
195
|
+
// Go to parent session
|
|
196
|
+
const parent = items.find(
|
|
197
|
+
(i) => i.type === 'session' && i.id === current.data.sessionId
|
|
198
|
+
);
|
|
199
|
+
if (parent) s.selectItem(parent.id, 'session', {}, items);
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── Enter — focus first detail field ─────────────────────────────────
|
|
205
|
+
if (e.key === 'Enter') {
|
|
206
|
+
e.preventDefault();
|
|
207
|
+
if (detailPanelRef.current) {
|
|
208
|
+
const firstInput = detailPanelRef.current.querySelector<HTMLElement>(
|
|
209
|
+
'input:not([disabled]),textarea:not([disabled])'
|
|
210
|
+
);
|
|
211
|
+
firstInput?.focus();
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── Escape ────────────────────────────────────────────────────────────
|
|
217
|
+
if (e.key === 'Escape') {
|
|
218
|
+
e.preventDefault();
|
|
219
|
+
if (s.filterQuery) {
|
|
220
|
+
s.setFilter('');
|
|
221
|
+
searchRef.current?.clear();
|
|
222
|
+
} else if (s.selectedIds.size > 1) {
|
|
223
|
+
s.clearSelection();
|
|
224
|
+
} else {
|
|
225
|
+
(document.activeElement as HTMLElement)?.blur();
|
|
226
|
+
}
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── Delete ────────────────────────────────────────────────────────────
|
|
231
|
+
if (e.key === 'Delete') {
|
|
232
|
+
e.preventDefault();
|
|
233
|
+
const ids = [...s.selectedIds].filter(
|
|
234
|
+
(key) => key !== `course:${s.course.id}`
|
|
235
|
+
);
|
|
236
|
+
if (!ids.length) return;
|
|
237
|
+
const count = ids.length;
|
|
238
|
+
s.showConfirm({
|
|
239
|
+
title: `Excluir ${count > 1 ? count + ' itens' : 'item selecionado'}?`,
|
|
240
|
+
description: 'Esta ação não pode ser desfeita.',
|
|
241
|
+
onConfirm: () => {
|
|
242
|
+
s.deleteSelected();
|
|
243
|
+
toast.success(
|
|
244
|
+
count > 1 ? `${count} itens excluídos` : 'Item excluído'
|
|
245
|
+
);
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── Ctrl/Cmd + C ─────────────────────────────────────────────────────
|
|
252
|
+
if (ctrl && e.key.toLowerCase() === 'c') {
|
|
253
|
+
e.preventDefault();
|
|
254
|
+
const selectedKeys = [...s.selectedIds].filter(
|
|
255
|
+
(key) => key.startsWith('lesson:') || key.startsWith('session:')
|
|
256
|
+
);
|
|
257
|
+
if (!selectedKeys.length) return;
|
|
258
|
+
const firstKey = selectedKeys[0]!;
|
|
259
|
+
const type = firstKey.startsWith('lesson:') ? 'lesson' : 'session';
|
|
260
|
+
const ids = selectedKeys.map((key) => key.slice(key.indexOf(':') + 1));
|
|
261
|
+
s.copyItems(ids, type);
|
|
262
|
+
toast.success(
|
|
263
|
+
`${ids.length > 1 ? ids.length + ' itens copiados' : 'Item copiado'}`
|
|
264
|
+
);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── Ctrl/Cmd + D — duplicate ──────────────────────────────────────────
|
|
269
|
+
if (ctrl && e.key.toLowerCase() === 'd') {
|
|
270
|
+
e.preventDefault();
|
|
271
|
+
if (onDuplicate) {
|
|
272
|
+
onDuplicate();
|
|
273
|
+
} else {
|
|
274
|
+
s.duplicateSelected();
|
|
275
|
+
toast.success('Item duplicado');
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ── Ctrl/Cmd + V — paste ──────────────────────────────────────────────
|
|
281
|
+
if (ctrl && e.key.toLowerCase() === 'v') {
|
|
282
|
+
e.preventDefault();
|
|
283
|
+
if (onPaste) {
|
|
284
|
+
onPaste();
|
|
285
|
+
} else {
|
|
286
|
+
toast('Nada para colar');
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ── Ctrl/Cmd + N — new item ───────────────────────────────────────────
|
|
292
|
+
if (ctrl && e.key.toLowerCase() === 'n') {
|
|
293
|
+
e.preventDefault();
|
|
294
|
+
if (s.activeItemType === 'course') {
|
|
295
|
+
s.addSession();
|
|
296
|
+
toast.success('Nova sessão criada');
|
|
297
|
+
} else if (s.activeItemType === 'session' && s.activeItemId) {
|
|
298
|
+
s.addLesson(s.activeItemId);
|
|
299
|
+
toast.success('Nova aula criada');
|
|
300
|
+
} else if (s.activeItemType === 'lesson') {
|
|
301
|
+
// Find parent session and add there
|
|
302
|
+
const lesson = s.lessons.find((l) => l.id === s.activeItemId);
|
|
303
|
+
if (lesson) {
|
|
304
|
+
s.addLesson(lesson.sessionId);
|
|
305
|
+
toast.success('Nova aula criada na mesma sessão');
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
[onToggleHelp, searchRef, detailPanelRef, onDuplicate, onPaste]
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
useEffect(() => {
|
|
315
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
316
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
317
|
+
}, [handleKeyDown]);
|
|
318
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { redirect } from 'next/navigation';
|
|
2
|
-
|
|
3
|
-
export default async function CourseStructureRedirectPage({
|
|
4
|
-
params,
|
|
5
|
-
}: {
|
|
6
|
-
params: Promise<{ id: string }>;
|
|
7
|
-
}) {
|
|
8
|
-
const { id } = await params;
|
|
9
|
-
redirect(`/lms/courses/${id}`);
|
|
10
|
-
}
|
|
1
|
+
import { redirect } from 'next/navigation';
|
|
2
|
+
|
|
3
|
+
export default async function CourseStructureRedirectPage({
|
|
4
|
+
params,
|
|
5
|
+
}: {
|
|
6
|
+
params: Promise<{ id: string }>;
|
|
7
|
+
}) {
|
|
8
|
+
const { id } = await params;
|
|
9
|
+
redirect(`/lms/courses/${id}`);
|
|
10
|
+
}
|
|
@@ -132,7 +132,8 @@ export function EnterpriseCourseCreateSheet({
|
|
|
132
132
|
setSaving(true);
|
|
133
133
|
try {
|
|
134
134
|
const payload = {
|
|
135
|
-
|
|
135
|
+
name: data.nomeInterno.trim(),
|
|
136
|
+
slug: data.slug.trim(),
|
|
136
137
|
title: data.tituloComercial,
|
|
137
138
|
description: data.descricao,
|
|
138
139
|
level: toApiLevel(data.nivel),
|