8585</template >
8686
8787<script setup>
88- import { watch , toRefs , nextTick } from " vue"
88+ import { watch , toRefs } from " vue"
8989import useResourceSelection from " ../../composables/coursemaintenance/useResourceSelection"
9090
9191const props = defineProps ({
@@ -103,7 +103,7 @@ const props = defineProps({
103103
104104const emit = defineEmits ([" update:modelValue" ])
105105
106- // hook with shared logic
106+ // Shared selection logic (composable)
107107const sel = useResourceSelection ()
108108const {
109109 tree ,
@@ -121,21 +121,123 @@ const {
121121 expandAll ,
122122} = sel
123123
124- // sync in/out
124+ /**
125+ * Transform the "Documents" group into a real folder/file tree using relative paths.
126+ * - Real folders (filetype === 'folder' or path ends with '/') remain as selectable only if they were.
127+ * - Synthetic folders (created by this function) are NOT selectable and use string ids (__dir__<rel>).
128+ * - Adds node.meta with the relative path to improve search filtering.
129+ * - Keeps everything else unchanged for other groups.
130+ */
131+ function treeifyDocuments (groups ) {
132+ const out = Array .isArray (groups) ? groups : []
133+ for (const g of out) {
134+ if (! g || g .type !== " document" ) continue
135+
136+ const flat = Array .isArray (g .children ) ? g .children : Array .isArray (g .items ) ? g .items : []
137+ if (! flat .length ) continue
138+
139+ // Compute relative path (strip "document/" prefix if present)
140+ const relOf = (n ) => {
141+ const raw = String (n? .extra ? .path || n? .label || " " ).trim ()
142+ if (! raw) return null
143+ let rel = raw .replace (/ ^ \/ ? document\/ ? / , " " ).replace (/ \\ / g , " /" )
144+ const isFolder = String (n? .extra ? .filetype || " " ).toLowerCase () === " folder" || / \/ $ / .test (rel)
145+ if (isFolder) rel = rel .replace (/ \/ + $ / , " " ) + " /"
146+ return rel .replace (/ ^ \/ + / , " " )
147+ }
148+
149+ // Index existing (real) folders
150+ const folderMap = new Map () // rel => node
151+ for (const it of flat) {
152+ const rel = relOf (it)
153+ if (! rel) continue
154+ const isFolder = String (it? .extra ? .filetype || " " ).toLowerCase () === " folder" || rel .endsWith (" /" )
155+ if (isFolder) {
156+ it .label = (rel .replace (/ \/ $ / , " " ).split (" /" ).pop () || " /" ) + " /"
157+ it .meta = rel
158+ it .children = Array .isArray (it .children ) ? it .children : []
159+ folderMap .set (rel, it)
160+ }
161+ }
162+
163+ // Create synthetic folder if missing for a given relative path
164+ const ensureFolder = (rel ) => {
165+ if (folderMap .has (rel)) return folderMap .get (rel)
166+ const name = rel .replace (/ \/ $ / , " " ).split (" /" ).pop () || " /"
167+ const synthetic = {
168+ id: ` __dir__${ rel} ` ,
169+ type: " document" ,
170+ label: name + " /" ,
171+ meta: rel,
172+ selectable: false , // synthetic folders shouldn't be checkable
173+ children: [],
174+ extra: { filetype: " folder" },
175+ }
176+ folderMap .set (rel, synthetic)
177+ return synthetic
178+ }
179+
180+ const parentRelOf = (rel , isFolder ) => {
181+ const clean = isFolder ? rel .replace (/ \/ + $ / , " " ) : rel
182+ const dir = clean .includes (" /" ) ? clean .slice (0 , clean .lastIndexOf (" /" )) : " "
183+ return dir ? dir + " /" : " "
184+ }
185+
186+ const root = { children: [] }
187+
188+ // Place each item under its parent folder chain
189+ for (const it of flat) {
190+ const rel = relOf (it)
191+ if (! rel) continue
192+ const isFolder = String (it? .extra ? .filetype || " " ).toLowerCase () === " folder" || rel .endsWith (" /" )
193+ const parentRel = parentRelOf (rel, isFolder)
194+ const parent = parentRel ? ensureFolder (parentRel) : root
195+
196+ if (isFolder) {
197+ const f = ensureFolder (rel)
198+ if (! parent .children .includes (f)) parent .children .push (f)
199+ } else {
200+ const n = { ... it, meta: rel, label: rel .split (" /" ).pop () }
201+ parent .children .push (n)
202+ }
203+ }
204+
205+ // Sort: folders first, then files (case-insensitive)
206+ const sortChildren = (list ) => {
207+ list .sort ((a , b ) => {
208+ const af = (a .extra ? .filetype || " " ).toLowerCase () === " folder" || / \/ $ / .test (a .label || " " )
209+ const bf = (b .extra ? .filetype || " " ).toLowerCase () === " folder" || / \/ $ / .test (b .label || " " )
210+ if (af !== bf) return af ? - 1 : 1
211+ return String (a .label || " " ).localeCompare (String (b .label || " " ), undefined , { sensitivity: " base" })
212+ })
213+ for (const n of list) if (n .children ? .length ) sortChildren (n .children )
214+ }
215+ sortChildren (root .children )
216+
217+ g .children = root .children
218+ delete g .items
219+ }
220+ return out
221+ }
222+
223+ // sync input => internal tree
125224const { groups , modelValue } = toRefs (props)
126225watch (
127226 groups,
128227 (arr ) => {
129- const norm = normalizeTreeForSelection (Array .isArray (arr) ? JSON .parse (JSON .stringify (arr)) : [])
130- // ensure top-level children
228+ // Normalize the incoming tree first (ensures leaves have id/type/selectable)
229+ let norm = normalizeTreeForSelection (Array .isArray (arr) ? JSON .parse (JSON .stringify (arr)) : [])
230+ // Build the folder/file hierarchy only for Documents
231+ norm = treeifyDocuments (norm)
232+ // Ensure top-level children exist
131233 tree .value = norm .map ((g ) =>
132234 Array .isArray (g .children ) ? g : { ... g, children: Array .isArray (g .items ) ? g .items : [] },
133235 )
134236 },
135237 { immediate: true },
136238)
137239
138- // auto expand on first data
240+ // auto expand on first data load
139241watch (tree, (v ) => {
140242 if (Array .isArray (v) && v .length ) {
141243 forceOpen .value = true
@@ -147,6 +249,7 @@ watch(tree, (v) => {
147249
148250let syncing = false
149251
252+ // v-model (in) -> local
150253watch (
151254 modelValue,
152255 (v ) => {
@@ -160,6 +263,7 @@ watch(
160263 { immediate: true },
161264)
162265
266+ // local -> v-model (out)
163267watch (
164268 selections,
165269 (v ) => {
@@ -175,7 +279,7 @@ watch(
175279< / script>
176280
177281< script>
178- /* inline child components to keep it self-contained */
282+ /* Inline child components to keep it self-contained */
179283export default {
180284 components: {
181285 GroupBlock: {
@@ -190,7 +294,7 @@ export default {
190294 },
191295 emits: [" select-group" ],
192296 components: {
193- /* <-- REGISTER TreeNode LOCALLY HERE */
297+ /* <-- Tree node item (no type badge; uses folder/file icon) */
194298 TreeNode: {
195299 name: " TreeNode" ,
196300 props: {
@@ -220,14 +324,12 @@ export default {
220324 onCheck (e ) {
221325 this .$emit (" toggle" , e .target .checked )
222326 },
223- badgeTone () {
224- return " bg-gray-10 text-gray-90 ring-gray-25"
225- },
226327 },
227328 template: `
228329 <li class="p-3 rounded-lg hover:bg-gray-15 transition">
229330 <div class="flex items-start gap-3">
230331
332+ <!-- Disclosure -->
231333 <div class="mt-0.5 w-5 flex items-center justify-center">
232334 <button
233335 class="text-gray-50 hover:text-gray-90"
@@ -238,20 +340,23 @@ export default {
238340 </button>
239341 </div>
240342
343+ <!-- Checkbox only for checkable nodes -->
241344 <template v-if="isNodeCheckable(node)">
242345 <input type="checkbox" :checked="checked" @change="onCheck" class="mt-0.5 chk-success"/>
243346 </template>
244347 <template v-else>
245348 <span class="mt-0.5 w-5"></span>
246349 </template>
247350
351+ <!-- Label with folder/file icon (no type badge) -->
248352 <div class="flex-1">
249353 <div class="flex items-center gap-2">
250- <span class="rounded px-2 py-0.5 text-xs font-semibold ring-1 ring-inset" :class="badgeTone()">
251- {{ (node.titleType || node.type || '').toUpperCase() }}
252- </span>
253- <span class="text-sm text-gray-90">{{ node.label || node.title || '—' }}</span>
254- <span v-if="node.meta" class="text-xs text-gray-50">· {{ node.meta }}</span>
354+ <i
355+ :class="((node.extra && node.extra.filetype === 'folder') || /\\ /$/.test(node.label || ''))
356+ ? 'mdi mdi-folder'
357+ : 'mdi mdi-file-outline'"></i>
358+ <span class="text-sm text-gray-90 break-all">{{ node.label || node.title || '—' }}</span>
359+ <span v-if="node.meta" class="text-xs text-gray-50 break-all">· {{ node.meta }}</span>
255360 </div>
256361
257362 <ul v-if="open && node.children && node.children.length" class="mt-2 ml-7 space-y-2">
0 commit comments