Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions assets/vue/components/documents/ResourceFileLink.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,14 @@ export default {
},
computed: {
getDataType() {
if (this.resource.resourceNode.firstResourceFile.image) {
const node = this.resource && this.resource.resourceNode
const file = node && node.firstResourceFile

if (file && file.image) {
return "image"
}
if (this.resource.resourceNode.firstResourceFile.video) {

if (file && file.video) {
return "video"
}

Expand Down
30 changes: 24 additions & 6 deletions assets/vue/components/documents/ResourceIcon.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@
icon="folder-generic"
/>
<BaseIcon
v-else-if="resourceData.resourceNode.firstResourceFile.image"
v-else-if="isImage(resourceData)"
icon="file-image"
/>
<BaseIcon
v-else-if="resourceData.resourceNode.firstResourceFile.video"
v-else-if="isVideo(resourceData)"
icon="file-video"
/>
<BaseIcon
v-else-if="resourceData.resourceNode.firstResourceFile.text"
v-else-if="hasTextFlag"
icon="file-text"
/>
<BaseIcon
v-else-if="'application/pdf' === resourceData.resourceNode.firstResourceFile.mimeType"
v-else-if="isPdfFile"
icon="file-pdf"
/>
<BaseIcon
Expand All @@ -30,15 +30,33 @@
</template>

<script setup>
import { computed } from "vue"
import BaseIcon from "../basecomponents/BaseIcon.vue"
import { useFileUtils } from "../../composables/fileUtils"

const { isAudio } = useFileUtils()
const { isImage, isVideo, isAudio } = useFileUtils()

defineProps({
const props = defineProps({
resourceData: {
type: Object,
required: true,
},
})

const hasTextFlag = computed(() => {
const file = props.resourceData?.resourceNode?.firstResourceFile
return !!file && !!file.text
})

const isPdfFile = computed(() => {
const file = props.resourceData?.resourceNode?.firstResourceFile

if (!file || !file.mimeType) {
return false
}

const mime = String(file.mimeType).split(";")[0].trim().toLowerCase()

return mime === "application/pdf"
})
</script>
197 changes: 145 additions & 52 deletions assets/vue/views/documents/DocumentsList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@
>
<template #body="slotProps">
{{
slotProps.data.resourceNode.firstResourceFile
slotProps.data.resourceNode && slotProps.data.resourceNode.firstResourceFile
? prettyBytes(slotProps.data.resourceNode.firstResourceFile.size)
: ""
}}
Expand Down Expand Up @@ -654,7 +654,14 @@ const showBackButtonIfNotRootFolder = computed(() => {
})

function goToAddVariation(item) {
const resourceFileId = item.resourceNode.firstResourceFile.id
const firstFile = item.resourceNode?.firstResourceFile
if (!firstFile) {
console.warn("Missing firstResourceFile for document", item.iid)
return
}

const resourceFileId = firstFile.id

router.push({
name: "DocumentsAddVariation",
params: { resourceFileId, node: route.params.node },
Expand Down Expand Up @@ -970,105 +977,139 @@ function recordedAudioNotSaved(error) {
console.error(error)
}

function openMoveDialog(document) {
item.value = document
isMoveDialogVisible.value = true
}

function openReplaceDialog(document) {
if (!canEdit(document)) {
return
/**
* -----------------------------------------
* MOVE: helpers + folders fetching
* -----------------------------------------
*/
function normalizeResourceNodeId(value) {
if (value == null) return null
if (typeof value === "number") return value

if (typeof value === "string") {
// Accept IRI like "/api/resource_nodes/123"
const iriMatch = value.match(/\/api\/resource_nodes\/(\d+)/)
if (iriMatch) return Number(iriMatch[1])

// Accept "123"
if (/^\d+$/.test(value)) return Number(value)
}

documentToReplace.value = document
isReplaceDialogVisible.value = true
return null
}

async function replaceDocument() {
if (!selectedReplaceFile.value) {
notification.showErrorNotification(t("No file selected."))
return
}
function getRootNodeIdForFolders() {
let node = resourceNode.value
let fallback =
normalizeResourceNodeId(node?.id) ??
normalizeResourceNodeId(route.params.node) ??
normalizeResourceNodeId(route.query.node)

if (!(documentToReplace.value.filetype === "file" || documentToReplace.value.filetype === "video")) {
notification.showErrorNotification(t("Only files can be replaced."))
return
while (node?.parent) {
if (node?.resourceType?.title === "courses") break
node = node.parent
}

const formData = new FormData()
formData.append("file", selectedReplaceFile.value)
try {
await axios.post(`/api/documents/${documentToReplace.value.iid}/replace`, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
})
notification.showSuccessNotification(t("File replaced"))
isReplaceDialogVisible.value = false
onUpdateOptions(options.value)
} catch (error) {
notification.showErrorNotification(t("Error replacing file."))
console.error(error)
}
return normalizeResourceNodeId(node?.id) ?? fallback
}

async function fetchFolders(nodeId = null, parentPath = "") {
const startId = normalizeResourceNodeId(nodeId || route.params.node || route.query.node)

const foldersList = [
{
label: t("Documents"),
value: nodeId || route.params.node || route.query.node || "root-node-id",
value: startId || "root-node-id",
},
]

try {
let nodesToFetch = [{ id: nodeId || route.params.node || route.query.node, path: parentPath }]
let nodesToFetch = [{ id: startId, path: parentPath }]
let depth = 0
const maxDepth = 5

while (nodesToFetch.length > 0 && depth < maxDepth) {
const currentNode = nodesToFetch.shift()
const currentNodeId = normalizeResourceNodeId(currentNode?.id)

if (!currentNodeId) {
depth++
continue
}

const response = await axios.get("/api/documents", {
params: {
filetype: "folder",
"resourceNode.parent": currentNode.id,
cid: route.query.cid,
sid: route.query.sid,
loadNode: 1,
filetype: ["folder"],
"resourceNode.parent": currentNodeId,
cid,
sid,
gid,
page: 1,
itemsPerPage: 200,
},
})

response.data["hydra:member"].forEach((folder) => {
const fullPath = `${currentNode.path}/${folder.title}`
const members = response.data?.["hydra:member"] || []

members.forEach((folder) => {
const folderNodeId =
normalizeResourceNodeId(folder?.resourceNode?.id) ?? normalizeResourceNodeId(folder?.resourceNodeId)

if (!folderNodeId) {
return
}

const fullPath = `${currentNode.path}/${folder.title}`.replace(/^\/+/, "")

foldersList.push({
label: fullPath,
value: folder.resourceNode?.id || folder.resourceNodeId || folder["@id"],
value: folderNodeId, // ALWAYS numeric
})

if (folder.resourceNode && folder.resourceNode.id) {
nodesToFetch.push({ id: folder.resourceNode.id, path: fullPath })
}
nodesToFetch.push({ id: folderNodeId, path: fullPath })
})

depth++
}

return foldersList
} catch (error) {
console.error("Error fetching folders:", error.message || error)
return []
console.error("Error fetching folders:", error?.message || error)
return foldersList
}
}

async function loadAllFolders() {
// Keep your behavior: start from current node.
// If you want ALWAYS from course root, tell me and I’ll adjust in 2 lines.
folders.value = await fetchFolders()
}

async function openMoveDialog(document) {
item.value = document
selectedFolder.value = null
await loadAllFolders()
isMoveDialogVisible.value = true
}

async function moveDocument() {
try {
const response = await axios.put(`/api/documents/${item.value.iid}/move`, {
parentResourceNodeId: selectedFolder.value,
})
const parentId = normalizeResourceNodeId(selectedFolder.value)

if (!parentId) {
notification.showErrorNotification(t("Select a folder"))
return
}

await axios.put(
`/api/documents/${item.value.iid}/move`,
{ parentResourceNodeId: parentId },
{
// IMPORTANT: backend needs context to move the correct resource_link
params: { cid, sid, gid },
},
)

notification.showSuccessNotification(t("Document moved successfully"))
isMoveDialogVisible.value = false
Expand All @@ -1079,6 +1120,53 @@ async function moveDocument() {
}
}

/**
* -----------------------------------------
* REPLACE
* -----------------------------------------
*/
function openReplaceDialog(document) {
if (!canEdit(document)) {
return
}

documentToReplace.value = document
isReplaceDialogVisible.value = true
}

async function replaceDocument() {
if (!selectedReplaceFile.value) {
notification.showErrorNotification(t("No file selected."))
return
}

if (!(documentToReplace.value.filetype === "file" || documentToReplace.value.filetype === "video")) {
notification.showErrorNotification(t("Only files can be replaced."))
return
}

const formData = new FormData()
formData.append("file", selectedReplaceFile.value)
try {
await axios.post(`/api/documents/${documentToReplace.value.iid}/replace`, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
})
notification.showSuccessNotification(t("File replaced"))
isReplaceDialogVisible.value = false
onUpdateOptions(options.value)
} catch (error) {
notification.showErrorNotification(t("Error replacing file."))
console.error(error)
}
}

/**
* -----------------------------------------
* CERTIFICATES
* -----------------------------------------
*/
async function selectAsDefaultCertificate(certificate) {
try {
const response = await axios.patch(`/gradebook/set_default_certificate/${cid}/${certificate.iid}`)
Expand Down Expand Up @@ -1106,6 +1194,11 @@ async function loadDefaultCertificate() {
}
}

/**
* -----------------------------------------
* TEMPLATE
* -----------------------------------------
*/
const showTemplateFormModal = ref(false)
const selectedFile = ref(null)
const templateFormData = ref({
Expand Down
Loading
Loading