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
656656function 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
10151016async 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
10631083async 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+
10671096async 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+ */
10821170async 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+ */
11091202const showTemplateFormModal = ref (false )
11101203const selectedFile = ref (null )
11111204const templateFormData = ref ({
0 commit comments