Skip to content

Commit 9dd64d0

Browse files
authored
Merge pull request #7139 from christianbeeznest/GH-6372-4
Documents: Safely attach shared files to multiple courses - refs #6372
2 parents 91f8d14 + 2ff4bb1 commit 9dd64d0

File tree

15 files changed

+1411
-406
lines changed

15 files changed

+1411
-406
lines changed

assets/vue/components/documents/ResourceFileLink.vue

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,14 @@ export default {
2525
},
2626
computed: {
2727
getDataType() {
28-
if (this.resource.resourceNode.firstResourceFile.image) {
28+
const node = this.resource && this.resource.resourceNode
29+
const file = node && node.firstResourceFile
30+
31+
if (file && file.image) {
2932
return "image"
3033
}
31-
if (this.resource.resourceNode.firstResourceFile.video) {
34+
35+
if (file && file.video) {
3236
return "video"
3337
}
3438

assets/vue/components/documents/ResourceIcon.vue

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,19 @@
44
icon="folder-generic"
55
/>
66
<BaseIcon
7-
v-else-if="resourceData.resourceNode.firstResourceFile.image"
7+
v-else-if="isImage(resourceData)"
88
icon="file-image"
99
/>
1010
<BaseIcon
11-
v-else-if="resourceData.resourceNode.firstResourceFile.video"
11+
v-else-if="isVideo(resourceData)"
1212
icon="file-video"
1313
/>
1414
<BaseIcon
15-
v-else-if="resourceData.resourceNode.firstResourceFile.text"
15+
v-else-if="hasTextFlag"
1616
icon="file-text"
1717
/>
1818
<BaseIcon
19-
v-else-if="'application/pdf' === resourceData.resourceNode.firstResourceFile.mimeType"
19+
v-else-if="isPdfFile"
2020
icon="file-pdf"
2121
/>
2222
<BaseIcon
@@ -30,15 +30,33 @@
3030
</template>
3131

3232
<script setup>
33+
import { computed } from "vue"
3334
import BaseIcon from "../basecomponents/BaseIcon.vue"
3435
import { useFileUtils } from "../../composables/fileUtils"
3536
36-
const { isAudio } = useFileUtils()
37+
const { isImage, isVideo, isAudio } = useFileUtils()
3738
38-
defineProps({
39+
const props = defineProps({
3940
resourceData: {
4041
type: Object,
4142
required: true,
4243
},
4344
})
45+
46+
const hasTextFlag = computed(() => {
47+
const file = props.resourceData?.resourceNode?.firstResourceFile
48+
return !!file && !!file.text
49+
})
50+
51+
const isPdfFile = computed(() => {
52+
const file = props.resourceData?.resourceNode?.firstResourceFile
53+
54+
if (!file || !file.mimeType) {
55+
return false
56+
}
57+
58+
const mime = String(file.mimeType).split(";")[0].trim().toLowerCase()
59+
60+
return mime === "application/pdf"
61+
})
4462
</script>

assets/vue/views/documents/DocumentsList.vue

Lines changed: 145 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@
146146
>
147147
<template #body="slotProps">
148148
{{
149-
slotProps.data.resourceNode.firstResourceFile
149+
slotProps.data.resourceNode && slotProps.data.resourceNode.firstResourceFile
150150
? prettyBytes(slotProps.data.resourceNode.firstResourceFile.size)
151151
: ""
152152
}}
@@ -654,7 +654,14 @@ const showBackButtonIfNotRootFolder = computed(() => {
654654
})
655655
656656
function goToAddVariation(item) {
657-
const resourceFileId = item.resourceNode.firstResourceFile.id
657+
const firstFile = item.resourceNode?.firstResourceFile
658+
if (!firstFile) {
659+
console.warn("Missing firstResourceFile for document", item.iid)
660+
return
661+
}
662+
663+
const resourceFileId = firstFile.id
664+
658665
router.push({
659666
name: "DocumentsAddVariation",
660667
params: { resourceFileId, node: route.params.node },
@@ -970,105 +977,139 @@ function recordedAudioNotSaved(error) {
970977
console.error(error)
971978
}
972979
973-
function openMoveDialog(document) {
974-
item.value = document
975-
isMoveDialogVisible.value = true
976-
}
977-
978-
function openReplaceDialog(document) {
979-
if (!canEdit(document)) {
980-
return
980+
/**
981+
* -----------------------------------------
982+
* MOVE: helpers + folders fetching
983+
* -----------------------------------------
984+
*/
985+
function normalizeResourceNodeId(value) {
986+
if (value == null) return null
987+
if (typeof value === "number") return value
988+
989+
if (typeof value === "string") {
990+
// Accept IRI like "/api/resource_nodes/123"
991+
const iriMatch = value.match(/\/api\/resource_nodes\/(\d+)/)
992+
if (iriMatch) return Number(iriMatch[1])
993+
994+
// Accept "123"
995+
if (/^\d+$/.test(value)) return Number(value)
981996
}
982997
983-
documentToReplace.value = document
984-
isReplaceDialogVisible.value = true
998+
return null
985999
}
9861000
987-
async function replaceDocument() {
988-
if (!selectedReplaceFile.value) {
989-
notification.showErrorNotification(t("No file selected."))
990-
return
991-
}
1001+
function getRootNodeIdForFolders() {
1002+
let node = resourceNode.value
1003+
let fallback =
1004+
normalizeResourceNodeId(node?.id) ??
1005+
normalizeResourceNodeId(route.params.node) ??
1006+
normalizeResourceNodeId(route.query.node)
9921007
993-
if (!(documentToReplace.value.filetype === "file" || documentToReplace.value.filetype === "video")) {
994-
notification.showErrorNotification(t("Only files can be replaced."))
995-
return
1008+
while (node?.parent) {
1009+
if (node?.resourceType?.title === "courses") break
1010+
node = node.parent
9961011
}
9971012
998-
const formData = new FormData()
999-
formData.append("file", selectedReplaceFile.value)
1000-
try {
1001-
await axios.post(`/api/documents/${documentToReplace.value.iid}/replace`, formData, {
1002-
headers: {
1003-
"Content-Type": "multipart/form-data",
1004-
},
1005-
})
1006-
notification.showSuccessNotification(t("File replaced"))
1007-
isReplaceDialogVisible.value = false
1008-
onUpdateOptions(options.value)
1009-
} catch (error) {
1010-
notification.showErrorNotification(t("Error replacing file."))
1011-
console.error(error)
1012-
}
1013+
return normalizeResourceNodeId(node?.id) ?? fallback
10131014
}
10141015
10151016
async function fetchFolders(nodeId = null, parentPath = "") {
1017+
const startId = normalizeResourceNodeId(nodeId || route.params.node || route.query.node)
1018+
10161019
const foldersList = [
10171020
{
10181021
label: t("Documents"),
1019-
value: nodeId || route.params.node || route.query.node || "root-node-id",
1022+
value: startId || "root-node-id",
10201023
},
10211024
]
10221025
10231026
try {
1024-
let nodesToFetch = [{ id: nodeId || route.params.node || route.query.node, path: parentPath }]
1027+
let nodesToFetch = [{ id: startId, path: parentPath }]
10251028
let depth = 0
10261029
const maxDepth = 5
10271030
10281031
while (nodesToFetch.length > 0 && depth < maxDepth) {
10291032
const currentNode = nodesToFetch.shift()
1033+
const currentNodeId = normalizeResourceNodeId(currentNode?.id)
1034+
1035+
if (!currentNodeId) {
1036+
depth++
1037+
continue
1038+
}
10301039
10311040
const response = await axios.get("/api/documents", {
10321041
params: {
1033-
filetype: "folder",
1034-
"resourceNode.parent": currentNode.id,
1035-
cid: route.query.cid,
1036-
sid: route.query.sid,
1042+
loadNode: 1,
1043+
filetype: ["folder"],
1044+
"resourceNode.parent": currentNodeId,
1045+
cid,
1046+
sid,
1047+
gid,
1048+
page: 1,
1049+
itemsPerPage: 200,
10371050
},
10381051
})
10391052
1040-
response.data["hydra:member"].forEach((folder) => {
1041-
const fullPath = `${currentNode.path}/${folder.title}`
1053+
const members = response.data?.["hydra:member"] || []
1054+
1055+
members.forEach((folder) => {
1056+
const folderNodeId =
1057+
normalizeResourceNodeId(folder?.resourceNode?.id) ?? normalizeResourceNodeId(folder?.resourceNodeId)
1058+
1059+
if (!folderNodeId) {
1060+
return
1061+
}
1062+
1063+
const fullPath = `${currentNode.path}/${folder.title}`.replace(/^\/+/, "")
10421064
10431065
foldersList.push({
10441066
label: fullPath,
1045-
value: folder.resourceNode?.id || folder.resourceNodeId || folder["@id"],
1067+
value: folderNodeId, // ALWAYS numeric
10461068
})
10471069
1048-
if (folder.resourceNode && folder.resourceNode.id) {
1049-
nodesToFetch.push({ id: folder.resourceNode.id, path: fullPath })
1050-
}
1070+
nodesToFetch.push({ id: folderNodeId, path: fullPath })
10511071
})
10521072
10531073
depth++
10541074
}
10551075
10561076
return foldersList
10571077
} catch (error) {
1058-
console.error("Error fetching folders:", error.message || error)
1059-
return []
1078+
console.error("Error fetching folders:", error?.message || error)
1079+
return foldersList
10601080
}
10611081
}
10621082
10631083
async function loadAllFolders() {
1084+
// Keep your behavior: start from current node.
1085+
// If you want ALWAYS from course root, tell me and I’ll adjust in 2 lines.
10641086
folders.value = await fetchFolders()
10651087
}
10661088
1089+
async function openMoveDialog(document) {
1090+
item.value = document
1091+
selectedFolder.value = null
1092+
await loadAllFolders()
1093+
isMoveDialogVisible.value = true
1094+
}
1095+
10671096
async function moveDocument() {
10681097
try {
1069-
const response = await axios.put(`/api/documents/${item.value.iid}/move`, {
1070-
parentResourceNodeId: selectedFolder.value,
1071-
})
1098+
const parentId = normalizeResourceNodeId(selectedFolder.value)
1099+
1100+
if (!parentId) {
1101+
notification.showErrorNotification(t("Select a folder"))
1102+
return
1103+
}
1104+
1105+
await axios.put(
1106+
`/api/documents/${item.value.iid}/move`,
1107+
{ parentResourceNodeId: parentId },
1108+
{
1109+
// IMPORTANT: backend needs context to move the correct resource_link
1110+
params: { cid, sid, gid },
1111+
},
1112+
)
10721113
10731114
notification.showSuccessNotification(t("Document moved successfully"))
10741115
isMoveDialogVisible.value = false
@@ -1079,6 +1120,53 @@ async function moveDocument() {
10791120
}
10801121
}
10811122
1123+
/**
1124+
* -----------------------------------------
1125+
* REPLACE
1126+
* -----------------------------------------
1127+
*/
1128+
function openReplaceDialog(document) {
1129+
if (!canEdit(document)) {
1130+
return
1131+
}
1132+
1133+
documentToReplace.value = document
1134+
isReplaceDialogVisible.value = true
1135+
}
1136+
1137+
async function replaceDocument() {
1138+
if (!selectedReplaceFile.value) {
1139+
notification.showErrorNotification(t("No file selected."))
1140+
return
1141+
}
1142+
1143+
if (!(documentToReplace.value.filetype === "file" || documentToReplace.value.filetype === "video")) {
1144+
notification.showErrorNotification(t("Only files can be replaced."))
1145+
return
1146+
}
1147+
1148+
const formData = new FormData()
1149+
formData.append("file", selectedReplaceFile.value)
1150+
try {
1151+
await axios.post(`/api/documents/${documentToReplace.value.iid}/replace`, formData, {
1152+
headers: {
1153+
"Content-Type": "multipart/form-data",
1154+
},
1155+
})
1156+
notification.showSuccessNotification(t("File replaced"))
1157+
isReplaceDialogVisible.value = false
1158+
onUpdateOptions(options.value)
1159+
} catch (error) {
1160+
notification.showErrorNotification(t("Error replacing file."))
1161+
console.error(error)
1162+
}
1163+
}
1164+
1165+
/**
1166+
* -----------------------------------------
1167+
* CERTIFICATES
1168+
* -----------------------------------------
1169+
*/
10821170
async function selectAsDefaultCertificate(certificate) {
10831171
try {
10841172
const response = await axios.patch(`/gradebook/set_default_certificate/${cid}/${certificate.iid}`)
@@ -1106,6 +1194,11 @@ async function loadDefaultCertificate() {
11061194
}
11071195
}
11081196
1197+
/**
1198+
* -----------------------------------------
1199+
* TEMPLATE
1200+
* -----------------------------------------
1201+
*/
11091202
const showTemplateFormModal = ref(false)
11101203
const selectedFile = ref(null)
11111204
const templateFormData = ref({

0 commit comments

Comments
 (0)