Skip to content

Commit f5fb2d2

Browse files
committed
Merge remote-tracking branch 'origin/master'
2 parents 9451eac + fa9f1aa commit f5fb2d2

File tree

16 files changed

+2507
-876
lines changed

16 files changed

+2507
-876
lines changed

assets/vue/components/coursemaintenance/ResourceSelector.vue

Lines changed: 121 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@
8585
</template>
8686

8787
<script setup>
88-
import { watch, toRefs, nextTick } from "vue"
88+
import { watch, toRefs } from "vue"
8989
import useResourceSelection from "../../composables/coursemaintenance/useResourceSelection"
9090
9191
const props = defineProps({
@@ -103,7 +103,7 @@ const props = defineProps({
103103
104104
const emit = defineEmits(["update:modelValue"])
105105
106-
// hook with shared logic
106+
// Shared selection logic (composable)
107107
const sel = useResourceSelection()
108108
const {
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
125224
const { groups, modelValue } = toRefs(props)
126225
watch(
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
139241
watch(tree, (v) => {
140242
if (Array.isArray(v) && v.length) {
141243
forceOpen.value = true
@@ -147,6 +249,7 @@ watch(tree, (v) => {
147249
148250
let syncing = false
149251
252+
// v-model (in) -> local
150253
watch(
151254
modelValue,
152255
(v) => {
@@ -160,6 +263,7 @@ watch(
160263
{ immediate: true },
161264
)
162265
266+
// local -> v-model (out)
163267
watch(
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 */
179283
export 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

Comments
 (0)