@hed-hog/lms 0.0.353 → 0.0.355

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.
Files changed (135) hide show
  1. package/dist/course/course-audio-transcription.service.d.ts +29 -0
  2. package/dist/course/course-audio-transcription.service.d.ts.map +1 -0
  3. package/dist/course/course-audio-transcription.service.js +291 -0
  4. package/dist/course/course-audio-transcription.service.js.map +1 -0
  5. package/dist/course/course-lesson.controller.d.ts +10 -0
  6. package/dist/course/course-lesson.controller.d.ts.map +1 -0
  7. package/dist/course/course-lesson.controller.js +62 -0
  8. package/dist/course/course-lesson.controller.js.map +1 -0
  9. package/dist/course/course-structure.controller.d.ts +41 -15
  10. package/dist/course/course-structure.controller.d.ts.map +1 -1
  11. package/dist/course/course-structure.controller.js +50 -6
  12. package/dist/course/course-structure.controller.js.map +1 -1
  13. package/dist/course/course-structure.service.d.ts +50 -15
  14. package/dist/course/course-structure.service.d.ts.map +1 -1
  15. package/dist/course/course-structure.service.js +238 -73
  16. package/dist/course/course-structure.service.js.map +1 -1
  17. package/dist/course/course-video-conversion.service.d.ts +20 -2
  18. package/dist/course/course-video-conversion.service.d.ts.map +1 -1
  19. package/dist/course/course-video-conversion.service.js +730 -10
  20. package/dist/course/course-video-conversion.service.js.map +1 -1
  21. package/dist/course/course.controller.d.ts +24 -8
  22. package/dist/course/course.controller.d.ts.map +1 -1
  23. package/dist/course/course.module.d.ts.map +1 -1
  24. package/dist/course/course.module.js +5 -3
  25. package/dist/course/course.module.js.map +1 -1
  26. package/dist/course/course.service.d.ts +24 -8
  27. package/dist/course/course.service.d.ts.map +1 -1
  28. package/dist/course/course.service.js +112 -176
  29. package/dist/course/course.service.js.map +1 -1
  30. package/dist/course/dto/create-course-lesson-frame.dto.d.ts +5 -0
  31. package/dist/course/dto/create-course-lesson-frame.dto.d.ts.map +1 -0
  32. package/dist/course/dto/create-course-lesson-frame.dto.js +30 -0
  33. package/dist/course/dto/create-course-lesson-frame.dto.js.map +1 -0
  34. package/dist/course/dto/create-course-structure-lesson.dto.d.ts +4 -2
  35. package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
  36. package/dist/course/dto/create-course-structure-lesson.dto.js +10 -3
  37. package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
  38. package/dist/course/dto/create-course.dto.d.ts +1 -1
  39. package/dist/course/dto/create-course.dto.d.ts.map +1 -1
  40. package/dist/course/dto/create-course.dto.js +6 -6
  41. package/dist/course/dto/create-course.dto.js.map +1 -1
  42. package/dist/course/dto/update-course-lesson-frame.dto.d.ts +5 -0
  43. package/dist/course/dto/update-course-lesson-frame.dto.d.ts.map +1 -0
  44. package/dist/course/dto/update-course-lesson-frame.dto.js +32 -0
  45. package/dist/course/dto/update-course-lesson-frame.dto.js.map +1 -0
  46. package/dist/course/dto/update-course-resources.dto.d.ts +4 -2
  47. package/dist/course/dto/update-course-resources.dto.d.ts.map +1 -1
  48. package/dist/course/dto/update-course-resources.dto.js +10 -3
  49. package/dist/course/dto/update-course-resources.dto.js.map +1 -1
  50. package/dist/course/dto/update-transcription-segments.dto.d.ts +10 -0
  51. package/dist/course/dto/update-transcription-segments.dto.d.ts.map +1 -0
  52. package/dist/course/dto/update-transcription-segments.dto.js +38 -0
  53. package/dist/course/dto/update-transcription-segments.dto.js.map +1 -0
  54. package/dist/course/lms-setting.controller.d.ts +13 -0
  55. package/dist/course/lms-setting.controller.d.ts.map +1 -0
  56. package/dist/course/lms-setting.controller.js +53 -0
  57. package/dist/course/lms-setting.controller.js.map +1 -0
  58. package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
  59. package/dist/enterprise/training/training-admin.service.js +74 -33
  60. package/dist/enterprise/training/training-admin.service.js.map +1 -1
  61. package/dist/index.d.ts +2 -0
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +2 -0
  64. package/dist/index.js.map +1 -1
  65. package/dist/lms.module.d.ts.map +1 -1
  66. package/dist/lms.module.js +6 -0
  67. package/dist/lms.module.js.map +1 -1
  68. package/hedhog/data/route.yaml +63 -0
  69. package/hedhog/data/setting_group.yaml +76 -0
  70. package/hedhog/frontend/app/courses/[id]/_components/CourseCertificateCard.tsx.ejs +3 -0
  71. package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +10 -1
  72. package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +7 -2
  73. package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +5 -2
  74. package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +7 -1
  75. package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +3 -0
  76. package/hedhog/frontend/app/courses/[id]/_components/CourseMultiEntityPicker.tsx.ejs +95 -50
  77. package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +11 -36
  78. package/hedhog/frontend/app/courses/[id]/_components/CourseSectionCard.tsx.ejs +19 -5
  79. package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +1 -1
  80. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +30 -10
  81. package/hedhog/frontend/app/courses/[id]/structure/_components/CourseInstructorsSummaryCard.tsx.ejs +95 -0
  82. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +29 -11
  83. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +42 -31
  84. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +10 -1
  85. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +51 -9
  86. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +25 -22
  87. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +12 -9
  88. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +315 -229
  89. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +3220 -534
  90. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +20 -15
  91. package/hedhog/frontend/app/courses/[id]/structure/_components/icon-action-tooltip.tsx.ejs +35 -0
  92. package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +76 -67
  93. package/hedhog/frontend/app/courses/[id]/structure/_components/search-filter.tsx.ejs +13 -10
  94. package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +18 -16
  95. package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +6 -0
  96. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +1 -1
  97. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +23 -11
  98. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-helpers.ts.ejs +48 -9
  99. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-course.tsx.ejs +11 -2
  100. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +121 -15
  101. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +69 -14
  102. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +11 -0
  103. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +31 -0
  104. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +32 -6
  105. package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +57 -3
  106. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +46 -6
  107. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +14 -0
  108. package/hedhog/frontend/app/courses/[id]/structure/_data/use-lesson-audio-files.ts.ejs +23 -0
  109. package/hedhog/frontend/app/courses/[id]/structure/_data/use-lms-settings-query.ts.ejs +34 -0
  110. package/hedhog/frontend/app/courses/[id]/structure/_data/use-transcription-segments.ts.ejs +76 -0
  111. package/hedhog/frontend/messages/en.json +39 -3
  112. package/hedhog/frontend/messages/pt.json +39 -3
  113. package/hedhog/table/course.yaml +8 -0
  114. package/hedhog/table/course_lesson_file.yaml +12 -4
  115. package/hedhog/table/course_lesson_transcription_segment.yaml +22 -0
  116. package/hedhog/table/course_lesson_video_frame.yaml +25 -0
  117. package/package.json +8 -8
  118. package/src/course/course-audio-transcription.service.ts +393 -0
  119. package/src/course/course-lesson.controller.ts +28 -0
  120. package/src/course/course-structure.controller.ts +49 -3
  121. package/src/course/course-structure.service.ts +294 -32
  122. package/src/course/course-video-conversion.service.ts +972 -6
  123. package/src/course/course.module.ts +5 -3
  124. package/src/course/course.service.ts +87 -139
  125. package/src/course/dto/create-course-lesson-frame.dto.ts +14 -0
  126. package/src/course/dto/create-course-structure-lesson.dto.ts +19 -3
  127. package/src/course/dto/create-course.dto.ts +5 -5
  128. package/src/course/dto/update-course-lesson-frame.dto.ts +16 -0
  129. package/src/course/dto/update-course-resources.dto.ts +18 -3
  130. package/src/course/dto/update-transcription-segments.dto.ts +20 -0
  131. package/src/course/lms-setting.controller.ts +30 -0
  132. package/src/enterprise/training/training-admin.service.ts +77 -24
  133. package/src/index.ts +2 -0
  134. package/src/lms.module.ts +6 -0
  135. package/hedhog/table/course_instructor.yaml +0 -27
@@ -48,6 +48,7 @@ import {
48
48
  useDeleteSessionMutation,
49
49
  useUpdateSessionMutation,
50
50
  } from '../_data/use-course-structure-mutations';
51
+ import { IconActionTooltip } from './icon-action-tooltip';
51
52
  import { useStructureStore } from './store';
52
53
  import type { Visibility } from './types';
53
54
 
@@ -161,22 +162,26 @@ export function EditorSession({ sessionId }: EditorSessionProps) {
161
162
  >
162
163
  {session.published ? 'Publicada' : 'Oculta'}
163
164
  </Badge>
164
- <Button
165
- type="button"
166
- variant="ghost"
167
- size="icon"
168
- className="size-7 text-destructive/60 hover:text-destructive shrink-0"
169
- title="Excluir sessão"
170
- aria-label="Excluir sessão"
171
- disabled={deleteSession.isPending}
172
- onClick={handleDelete}
165
+ <IconActionTooltip
166
+ label="Excluir sessão"
167
+ asWrapper={deleteSession.isPending}
173
168
  >
174
- {deleteSession.isPending ? (
175
- <Loader2 className="size-3.5 animate-spin" />
176
- ) : (
177
- <Trash2 className="size-3.5" />
178
- )}
179
- </Button>
169
+ <Button
170
+ type="button"
171
+ variant="ghost"
172
+ size="icon"
173
+ className="size-7 text-destructive/60 hover:text-destructive shrink-0"
174
+ aria-label="Excluir sessão"
175
+ disabled={deleteSession.isPending}
176
+ onClick={handleDelete}
177
+ >
178
+ {deleteSession.isPending ? (
179
+ <Loader2 className="size-3.5 animate-spin" />
180
+ ) : (
181
+ <Trash2 className="size-3.5" />
182
+ )}
183
+ </Button>
184
+ </IconActionTooltip>
180
185
  </div>
181
186
 
182
187
  {/* ── Scrollable body ───────────────────────────────────────────────── */}
@@ -0,0 +1,35 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode } from 'react';
4
+
5
+ import {
6
+ Tooltip,
7
+ TooltipContent,
8
+ TooltipProvider,
9
+ TooltipTrigger,
10
+ } from '@/components/ui/tooltip';
11
+
12
+ type IconActionTooltipProps = {
13
+ label: ReactNode;
14
+ children: ReactNode;
15
+ side?: 'top' | 'right' | 'bottom' | 'left';
16
+ asWrapper?: boolean;
17
+ };
18
+
19
+ export function IconActionTooltip({
20
+ label,
21
+ children,
22
+ side = 'top',
23
+ asWrapper = false,
24
+ }: IconActionTooltipProps) {
25
+ return (
26
+ <TooltipProvider>
27
+ <Tooltip>
28
+ <TooltipTrigger asChild>
29
+ {asWrapper ? <span className="inline-flex">{children}</span> : children}
30
+ </TooltipTrigger>
31
+ <TooltipContent side={side}>{label}</TooltipContent>
32
+ </Tooltip>
33
+ </TooltipProvider>
34
+ );
35
+ }
@@ -12,6 +12,7 @@ import {
12
12
  useDuplicateSessionMutation,
13
13
  useMoveLessonsMutation,
14
14
  } from '../_data/use-course-structure-mutations';
15
+ import { IconActionTooltip } from './icon-action-tooltip';
15
16
  import { useStructureStore } from './store';
16
17
 
17
18
  /**
@@ -175,92 +176,100 @@ export function MultiSelectBar() {
175
176
  <div className="flex-1" />
176
177
 
177
178
  {/* Copy */}
178
- <Button
179
- variant="ghost"
180
- size="icon"
181
- className="size-6 text-muted-foreground hover:text-foreground"
182
- title={t('copyTitle')}
183
- aria-label={t('copyAria')}
184
- onClick={handleCopy}
185
- >
186
- <Copy className="size-3" />
187
- </Button>
179
+ <IconActionTooltip label={t('copyTitle')}>
180
+ <Button
181
+ variant="ghost"
182
+ size="icon"
183
+ className="size-6 text-muted-foreground hover:text-foreground"
184
+ aria-label={t('copyAria')}
185
+ onClick={handleCopy}
186
+ >
187
+ <Copy className="size-3" />
188
+ </Button>
189
+ </IconActionTooltip>
188
190
 
189
191
  {/* Duplicate */}
190
- <Button
191
- variant="ghost"
192
- size="icon"
193
- className="size-6 text-muted-foreground hover:text-foreground"
194
- title={t('duplicateTitle')}
195
- aria-label={t('duplicateAria')}
196
- disabled={isDuplicating}
197
- onClick={handleDuplicate}
198
- >
199
- {isDuplicating ? (
200
- <Loader2 className="size-3 animate-spin" />
201
- ) : (
202
- <svg
203
- viewBox="0 0 24 24"
204
- className="size-3 fill-none stroke-current stroke-2"
205
- strokeLinecap="round"
206
- strokeLinejoin="round"
207
- aria-hidden
208
- >
209
- <rect x="9" y="9" width="13" height="13" rx="2" />
210
- <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
211
- </svg>
212
- )}
213
- </Button>
214
-
215
- {/* Move (lessons only) */}
216
- {canMove && (
192
+ <IconActionTooltip label={t('duplicateTitle')} asWrapper={isDuplicating}>
217
193
  <Button
218
194
  variant="ghost"
219
195
  size="icon"
220
196
  className="size-6 text-muted-foreground hover:text-foreground"
221
- title={t('moveTitle')}
222
- aria-label={t('moveAria')}
223
- disabled={isMoving}
224
- onClick={handleMove}
197
+ aria-label={t('duplicateAria')}
198
+ disabled={isDuplicating}
199
+ onClick={handleDuplicate}
225
200
  >
226
- {isMoving ? (
201
+ {isDuplicating ? (
227
202
  <Loader2 className="size-3 animate-spin" />
228
203
  ) : (
229
- <FolderOpen className="size-3" />
204
+ <svg
205
+ viewBox="0 0 24 24"
206
+ className="size-3 fill-none stroke-current stroke-2"
207
+ strokeLinecap="round"
208
+ strokeLinejoin="round"
209
+ aria-hidden
210
+ >
211
+ <rect x="9" y="9" width="13" height="13" rx="2" />
212
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
213
+ </svg>
230
214
  )}
231
215
  </Button>
216
+ </IconActionTooltip>
217
+
218
+ {/* Move (lessons only) */}
219
+ {canMove && (
220
+ <IconActionTooltip label={t('moveTitle')} asWrapper={isMoving}>
221
+ <Button
222
+ variant="ghost"
223
+ size="icon"
224
+ className="size-6 text-muted-foreground hover:text-foreground"
225
+ aria-label={t('moveAria')}
226
+ disabled={isMoving}
227
+ onClick={handleMove}
228
+ >
229
+ {isMoving ? (
230
+ <Loader2 className="size-3 animate-spin" />
231
+ ) : (
232
+ <FolderOpen className="size-3" />
233
+ )}
234
+ </Button>
235
+ </IconActionTooltip>
232
236
  )}
233
237
 
234
238
  {/* Delete */}
235
- <Button
236
- variant="ghost"
237
- size="icon"
238
- className="size-6 text-destructive/60 hover:text-destructive"
239
- title={t('deleteActionTitle')}
240
- aria-label={t('deleteActionAria')}
241
- disabled={bulkDelete.isPending}
242
- onClick={handleDelete}
239
+ <IconActionTooltip
240
+ label={t('deleteActionTitle')}
241
+ asWrapper={bulkDelete.isPending}
243
242
  >
244
- {bulkDelete.isPending ? (
245
- <Loader2 className="size-3 animate-spin" />
246
- ) : (
247
- <Trash2 className="size-3" />
248
- )}
249
- </Button>
243
+ <Button
244
+ variant="ghost"
245
+ size="icon"
246
+ className="size-6 text-destructive/60 hover:text-destructive"
247
+ aria-label={t('deleteActionAria')}
248
+ disabled={bulkDelete.isPending}
249
+ onClick={handleDelete}
250
+ >
251
+ {bulkDelete.isPending ? (
252
+ <Loader2 className="size-3 animate-spin" />
253
+ ) : (
254
+ <Trash2 className="size-3" />
255
+ )}
256
+ </Button>
257
+ </IconActionTooltip>
250
258
 
251
259
  <Separator orientation="vertical" className="mx-0.5 h-3.5" />
252
260
 
253
261
  {/* Deselect */}
254
- <Button
255
- variant="ghost"
256
- size="icon"
257
- className="size-6 text-muted-foreground hover:text-foreground"
258
- title={t('clearTitle')}
259
- aria-label={t('clearAria')}
260
- onClick={clearSelection}
261
- >
262
- <X className="size-3" />
263
- </Button>
262
+ <IconActionTooltip label={t('clearTitle')}>
263
+ <Button
264
+ variant="ghost"
265
+ size="icon"
266
+ className="size-6 text-muted-foreground hover:text-foreground"
267
+ aria-label={t('clearAria')}
268
+ onClick={clearSelection}
269
+ >
270
+ <X className="size-3" />
271
+ </Button>
272
+ </IconActionTooltip>
264
273
  </div>
265
274
  );
266
275
  }
@@ -14,6 +14,7 @@ import { Button } from '@/components/ui/button';
14
14
  import { Input } from '@/components/ui/input';
15
15
  import { cn } from '@/lib/utils';
16
16
  import { useDebounce } from '@/hooks/use-debounce';
17
+ import { IconActionTooltip } from './icon-action-tooltip';
17
18
  import { useStructureStore } from './store';
18
19
 
19
20
  export interface SearchFilterHandle {
@@ -77,15 +78,17 @@ export const SearchFilter = forwardRef<SearchFilterHandle, SearchFilterProps>(
77
78
  {resultCount}
78
79
  </span>
79
80
  )}
80
- <Button
81
- variant="ghost"
82
- size="icon"
83
- className="size-5 rounded-sm shrink-0"
84
- onClick={() => setValue('')}
85
- aria-label="Limpar busca"
86
- >
87
- <X className="size-3" />
88
- </Button>
81
+ <IconActionTooltip label="Limpar busca">
82
+ <Button
83
+ variant="ghost"
84
+ size="icon"
85
+ className="size-5 rounded-sm shrink-0"
86
+ onClick={() => setValue('')}
87
+ aria-label="Limpar busca"
88
+ >
89
+ <X className="size-3" />
90
+ </Button>
91
+ </IconActionTooltip>
89
92
  </div>
90
93
  )}
91
94
  </div>
@@ -93,4 +96,4 @@ export const SearchFilter = forwardRef<SearchFilterHandle, SearchFilterProps>(
93
96
  }
94
97
  );
95
98
 
96
- SearchFilter.displayName = 'SearchFilter';
99
+ SearchFilter.displayName = 'SearchFilter';
@@ -12,6 +12,7 @@ import {
12
12
  } from '@/components/ui/sheet';
13
13
  import { ResizableSheetContent } from '@/components/ui/resizable-sheet-content';
14
14
  import { Separator } from '@/components/ui/separator';
15
+ import { IconActionTooltip } from './icon-action-tooltip';
15
16
 
16
17
  // ── Data ──────────────────────────────────────────────────────────────────────
17
18
 
@@ -167,22 +168,23 @@ export function ShortcutsHelp({ open, onOpenChange }: ShortcutsHelpProps) {
167
168
  export function ShortcutsHelpTrigger({ onOpen }: { onOpen: () => void }) {
168
169
  const t = useTranslations('lms.CoursesPage.StructurePage.shortcuts');
169
170
  return (
170
- <Button
171
- variant="ghost"
172
- size="sm"
173
- className="h-8 gap-1.5 px-2 text-xs text-muted-foreground hover:text-foreground sm:px-3"
174
- onClick={onOpen}
175
- title={t('triggerTitle')}
176
- aria-label={t('triggerTitle')}
177
- >
178
- <Keyboard className="size-3.5" />
179
- <span className="sr-only sm:not-sr-only">{t('triggerLabel')}</span>
180
- <Badge
181
- variant="outline"
182
- className="ml-0.5 hidden h-4 px-1 text-[0.6rem] font-mono sm:inline-flex"
171
+ <IconActionTooltip label={t('triggerTitle')} side="bottom">
172
+ <Button
173
+ variant="ghost"
174
+ size="sm"
175
+ className="h-8 gap-1.5 px-2 text-xs text-muted-foreground hover:text-foreground sm:px-3"
176
+ onClick={onOpen}
177
+ aria-label={t('triggerTitle')}
183
178
  >
184
- Ctrl+/
185
- </Badge>
186
- </Button>
179
+ <Keyboard className="size-3.5" />
180
+ <span className="sr-only sm:not-sr-only">{t('triggerLabel')}</span>
181
+ <Badge
182
+ variant="outline"
183
+ className="ml-0.5 hidden h-4 px-1 text-[0.6rem] font-mono sm:inline-flex"
184
+ >
185
+ Ctrl+/
186
+ </Badge>
187
+ </Button>
188
+ </IconActionTooltip>
187
189
  );
188
190
  }
@@ -15,6 +15,10 @@ interface SortableTreeRowProps {
15
15
  isMatched: boolean;
16
16
  isEffectivelyExpanded: boolean;
17
17
  lessonCountMap: Map<string, number>;
18
+ sessionIndicatorMap: Map<
19
+ string,
20
+ { resourceCount: number; videoCount: number }
21
+ >;
18
22
  visibleItems: FlatItem[];
19
23
  /** When true (filter active) drag is disabled. */
20
24
  dragDisabled: boolean;
@@ -28,6 +32,7 @@ export function SortableTreeRow({
28
32
  isMatched,
29
33
  isEffectivelyExpanded,
30
34
  lessonCountMap,
35
+ sessionIndicatorMap,
31
36
  visibleItems,
32
37
  dragDisabled,
33
38
  }: SortableTreeRowProps) {
@@ -72,6 +77,7 @@ export function SortableTreeRow({
72
77
  isMatched={isMatched}
73
78
  isEffectivelyExpanded={isEffectivelyExpanded}
74
79
  lessonCountMap={lessonCountMap}
80
+ sessionIndicatorMap={sessionIndicatorMap}
75
81
  visibleItems={visibleItems}
76
82
  />
77
83
  </div>
@@ -558,7 +558,7 @@ export function TreeRootContextMenu({
558
558
 
559
559
  return (
560
560
  <ContextMenu>
561
- <ContextMenuTrigger className="block h-full w-full">
561
+ <ContextMenuTrigger className="flex flex-col flex-1 min-h-0 w-full">
562
562
  {children}
563
563
  </ContextMenuTrigger>
564
564
  <ContextMenuContent className="w-56">
@@ -9,6 +9,12 @@ import {
9
9
  } from '@/components/ui/popover';
10
10
  import { Separator } from '@/components/ui/separator';
11
11
  import { Switch } from '@/components/ui/switch';
12
+ import {
13
+ Tooltip,
14
+ TooltipContent,
15
+ TooltipProvider,
16
+ TooltipTrigger,
17
+ } from '@/components/ui/tooltip';
12
18
  import { SlidersHorizontal } from 'lucide-react';
13
19
  import { useTranslations } from 'next-intl';
14
20
 
@@ -32,17 +38,23 @@ export function TreeDisplaySettingsPopover() {
32
38
 
33
39
  return (
34
40
  <Popover>
35
- <PopoverTrigger asChild>
36
- <Button
37
- variant="ghost"
38
- size="sm"
39
- className="h-8 gap-1.5 text-xs text-muted-foreground hover:text-foreground"
40
- title={t('title')}
41
- >
42
- <SlidersHorizontal className="size-3.5" />
43
- <span className="sr-only sm:not-sr-only">{t('label')}</span>
44
- </Button>
45
- </PopoverTrigger>
41
+ <TooltipProvider>
42
+ <Tooltip>
43
+ <TooltipTrigger asChild>
44
+ <PopoverTrigger asChild>
45
+ <Button
46
+ variant="ghost"
47
+ size="sm"
48
+ className="h-8 gap-1.5 text-xs text-muted-foreground hover:text-foreground"
49
+ >
50
+ <SlidersHorizontal className="size-3.5" />
51
+ <span className="sr-only sm:not-sr-only">{t('label')}</span>
52
+ </Button>
53
+ </PopoverTrigger>
54
+ </TooltipTrigger>
55
+ <TooltipContent side="bottom">{t('title')}</TooltipContent>
56
+ </Tooltip>
57
+ </TooltipProvider>
46
58
 
47
59
  <PopoverContent align="end" className="w-56 p-0">
48
60
  <div className="px-3 py-2.5 border-b">
@@ -1,5 +1,30 @@
1
1
  import type { Course, FlatItem, Lesson, Session } from './types';
2
2
 
3
+ export interface SessionIndicatorCounts {
4
+ resourceCount: number;
5
+ videoCount: number;
6
+ }
7
+
8
+ export function isUploadedVideoResourceType(type: string): boolean {
9
+ return (
10
+ type === 'video_original' ||
11
+ type.startsWith('video_profile:') ||
12
+ type.startsWith('video/')
13
+ );
14
+ }
15
+
16
+ export function getLessonUploadedVideoCount(lesson: Lesson): number {
17
+ return lesson.resources.filter((resource) =>
18
+ isUploadedVideoResourceType(resource.type)
19
+ ).length;
20
+ }
21
+
22
+ export function getLessonAttachedResourceCount(lesson: Lesson): number {
23
+ return lesson.resources.filter(
24
+ (resource) => !isUploadedVideoResourceType(resource.type)
25
+ ).length;
26
+ }
27
+
3
28
  // ─────────────────────────────────────────────────────────────────────────────
4
29
  // Text Highlight Helpers
5
30
  // ─────────────────────────────────────────────────────────────────────────────
@@ -52,17 +77,11 @@ function lessonMatchesQuery(l: Lesson, q: string): boolean {
52
77
  }
53
78
 
54
79
  function sessionMatchesQuery(s: Session, q: string): boolean {
55
- return (
56
- s.title.toLowerCase().includes(q) ||
57
- s.code.toLowerCase().includes(q)
58
- );
80
+ return s.title.toLowerCase().includes(q) || s.code.toLowerCase().includes(q);
59
81
  }
60
82
 
61
83
  function courseMatchesQuery(c: Course, q: string): boolean {
62
- return (
63
- c.title.toLowerCase().includes(q) ||
64
- c.code.toLowerCase().includes(q)
65
- );
84
+ return c.title.toLowerCase().includes(q) || c.code.toLowerCase().includes(q);
66
85
  }
67
86
 
68
87
  /**
@@ -179,4 +198,24 @@ export function buildLessonCountMap(lessons: Lesson[]): Map<string, number> {
179
198
  map.set(lesson.sessionId, (map.get(lesson.sessionId) ?? 0) + 1);
180
199
  }
181
200
  return map;
182
- }
201
+ }
202
+
203
+ export function buildSessionIndicatorMap(
204
+ lessons: Lesson[]
205
+ ): Map<string, SessionIndicatorCounts> {
206
+ const map = new Map<string, SessionIndicatorCounts>();
207
+
208
+ for (const lesson of lessons) {
209
+ const current = map.get(lesson.sessionId) ?? {
210
+ resourceCount: 0,
211
+ videoCount: 0,
212
+ };
213
+
214
+ current.resourceCount += getLessonAttachedResourceCount(lesson);
215
+ current.videoCount += getLessonUploadedVideoCount(lesson);
216
+
217
+ map.set(lesson.sessionId, current);
218
+ }
219
+
220
+ return map;
221
+ }
@@ -3,6 +3,7 @@
3
3
  import { cn } from '@/lib/utils';
4
4
  import { BookOpen } from 'lucide-react';
5
5
  import { HighlightedText } from './highlighted-text';
6
+ import { useTreeDisplaySettings } from './use-tree-display-settings';
6
7
  import type { Course } from './types';
7
8
 
8
9
  interface TreeRowCourseProps {
@@ -21,6 +22,8 @@ export function TreeRowCourse({
21
22
  query,
22
23
  onClick,
23
24
  }: TreeRowCourseProps) {
25
+ const { showCode } = useTreeDisplaySettings();
26
+
24
27
  return (
25
28
  <div
26
29
  onClick={onClick}
@@ -28,7 +31,8 @@ export function TreeRowCourse({
28
31
  aria-selected={isActive}
29
32
  className={cn(
30
33
  'flex items-center gap-2 px-3 py-1.5 rounded-md cursor-pointer select-none transition-colors text-sm font-semibold h-full',
31
- isActive && 'bg-accent text-accent-foreground ring-1 ring-inset ring-primary/20',
34
+ isActive &&
35
+ 'bg-accent text-accent-foreground ring-1 ring-inset ring-primary/20',
32
36
  isSelected && !isActive && 'bg-accent/60',
33
37
  !isActive && !isSelected && 'hover:bg-muted/60'
34
38
  )}
@@ -37,6 +41,11 @@ export function TreeRowCourse({
37
41
  <span className="truncate flex-1">
38
42
  <HighlightedText text={data.title} query={query} />
39
43
  </span>
44
+ {showCode && (
45
+ <span className="inline-flex items-center rounded border border-border/60 bg-muted/60 px-1 text-[0.6rem] font-mono text-muted-foreground leading-4 shrink-0">
46
+ {data.code}
47
+ </span>
48
+ )}
40
49
  <span
41
50
  className={cn(
42
51
  'text-[0.6rem] px-1.5 py-0.5 rounded-full shrink-0 font-medium leading-none',
@@ -49,4 +58,4 @@ export function TreeRowCourse({
49
58
  </span>
50
59
  </div>
51
60
  );
52
- }
61
+ }