Skip to content
Merged
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
285 changes: 160 additions & 125 deletions assets/vue/views/filemanager/Upload.vue
Original file line number Diff line number Diff line change
@@ -1,31 +1,38 @@
<template>
<BaseToolbar v-if="!embedded">
<BaseButton :label="t('Back')" icon="back" type="black" @click="back" />
<BaseButton
:label="t('Back')"
icon="back"
type="black"
@click="back"
/>
</BaseToolbar>

<div class="flex flex-col justify-center items-center">
<div class="mb-4 w-full">
<Dashboard
v-if="uppy"
:plugins="['Webcam', 'ImageEditor']"
:props="{
proudlyDisplayPoweredByUppy: false,
width: '100%',
height: '350px',
}"
:uppy="uppy"
:proudlyDisplayPoweredByUppy="false"
:width="'100%'"
:height="'350px'"
/>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue"
import { ref, onBeforeUnmount } from "vue"
import "@uppy/core/dist/style.css"
import "@uppy/dashboard/dist/style.css"
import "@uppy/image-editor/dist/style.css"
import "@uppy/webcam/dist/style.css"
import Uppy from "@uppy/core"
import Webcam from "@uppy/webcam"
import { Dashboard } from "@uppy/vue"

const Webcam = require("@uppy/webcam").default
const XHRUpload = require("@uppy/xhr-upload").default
const ImageEditor = require("@uppy/image-editor").default
import XHRUpload from "@uppy/xhr-upload"
import ImageEditor from "@uppy/image-editor"

import { useRouter } from "vue-router"
import { ENTRYPOINT } from "../../config/entrypoint"
Expand All @@ -48,25 +55,30 @@ const { gid, sid, cid } = useCidReq()
const { onCreated } = useUpload()
const { t } = useI18n()

const LOG_PREFIX = "[UPLOAD DBG]"
function log(...args) { console.log(LOG_PREFIX, ...args) }
const LOG_PREFIX = "[FILEMANAGER UPLOAD]"
function log(...args) {
// Keep logs in English for easier debugging
console.log(LOG_PREFIX, ...args)
}

function resolveParentFromSessionThenProp() {
/**
* Resolve parent node from session storage (pf_parent) or component props.
* This mirrors the legacy file manager behavior.
*/
function resolveParentResourceNodeId() {
try {
const ssRaw = sessionStorage.getItem("pf_parent")
const ss = Number(ssRaw || 0)
if (ss) {
return ss
const pfParentRaw = sessionStorage.getItem("pf_parent")
const pfParent = Number(pfParentRaw || 0)
if (pfParent) {
return pfParent
}
const p = Number(props.parentResourceNodeId || 0)
return p || 0
} catch (e) {
return Number(props.parentResourceNodeId || 0)
log("Failed to read pf_parent from sessionStorage, falling back to props", e)
}
}

const parentIdRef = ref(0)
const fileTypeRef = ref(String(props.filetype || "file"))
const fromProps = Number(props.parentResourceNodeId || 0)
return fromProps || 0
}

function buildEndpoint(parentId) {
const pid = String(Number(parentId || 0))
Expand All @@ -75,122 +87,145 @@ function buildEndpoint(parentId) {
parentResourceNodeId: pid,
parent: pid,
}).toString()
const ep = `${ENTRYPOINT}personal_files?${qs}`
return ep

return `${ENTRYPOINT}personal_files?${qs}`
}

const parentResourceNodeId = ref(resolveParentResourceNodeId())
const uploadedItems = ref([])
const uppy = ref(null)

function applyCurrentParentToUppy(hook = "") {
const freshParent = resolveParentFromSessionThenProp()
parentIdRef.value = freshParent

const plugin = uppy.value?.getPlugin?.("XHRUpload")

if (plugin?.setOptions) {
const newEndpoint = buildEndpoint(freshParent)
plugin.setOptions({ endpoint: newEndpoint })
}

const beforeMeta = uppy.value?.getMeta?.() || {}
uppy.value?.setMeta({
filetype: fileTypeRef.value,
parentResourceNodeId: String(freshParent),
parentResourceNode: `/api/resource_nodes/${freshParent}`,
"resourceNode.parent": String(freshParent),
resourceLinkList: JSON.stringify([{ gid, sid, cid, visibility: RESOURCE_LINK_PUBLISHED }]),
isUncompressZipEnabled: false,
fileExistsOption: "rename",
const allowedFiletypes = ["file", "video", "certificate"]
const filetype = allowedFiletypes.includes(props.filetype) ? props.filetype : "file"

const resourceLinkList = JSON.stringify([
{
gid,
sid,
cid,
visibility: RESOURCE_LINK_PUBLISHED,
},
])

// Advanced options defaults – we do not expose UI here, just send sane defaults
const isUncompressZipEnabled = false
const fileExistsOption = "rename"

/**
* Single shared Uppy instance (same pattern as DocumentUpload.vue).
* This avoids reactivity wrappers around the instance.
*/
const uppy = new Uppy({ autoProceed: false })
.use(Webcam)
.use(ImageEditor, {
cropperOptions: {
viewMode: 1,
background: false,
autoCropArea: 1,
responsive: true,
},
actions: {
revert: true,
rotate: true,
granularRotate: true,
flip: true,
zoomIn: true,
zoomOut: true,
cropSquare: true,
cropWidescreen: true,
cropWidescreenVertical: true,
},
})
const afterMeta = uppy.value?.getMeta?.() || {}
const ep = plugin?.opts?.endpoint
}

onMounted(() => {
parentIdRef.value = resolveParentFromSessionThenProp()
uppy.value = new Uppy({
autoProceed: true,
debug: true,
restrictions: { allowedFileTypes: fileTypeRef.value === "certificate" ? [".html"] : null },
.use(XHRUpload, {
endpoint: buildEndpoint(parentResourceNodeId.value),
formData: true,
fieldName: "uploadFile",
allowedMetaFields: [
"filetype",
"parentResourceNodeId",
"parentResourceNode",
"resourceNode.parent",
"resourceLinkList",
"isUncompressZipEnabled",
"fileExistsOption",
],
})
.use(ImageEditor, {
cropperOptions: { viewMode: 1, background: false, autoCropArea: 1, responsive: true },
actions: {
revert: true, rotate: true, granularRotate: true, flip: true,
zoomIn: true, zoomOut: true, cropSquare: true, cropWidescreen: true, cropWidescreenVertical: true,
},
})
.use(XHRUpload, {
endpoint: buildEndpoint(parentIdRef.value),
formData: true,
fieldName: "uploadFile",
allowedMetaFields: [
"filetype",
"parentResourceNodeId",
"parentResourceNode",
"resourceNode.parent",
"resourceLinkList",
"isUncompressZipEnabled",
"fileExistsOption",
],
})
.on("file-added", (file) => {
applyCurrentParentToUppy("file-added")
})
.on("upload", (data) => {
applyCurrentParentToUppy("upload")
})
.on("upload-progress", (file, progress) => {
})
.on("upload-success", (file, response) => {
onCreated(response?.body)
if (response?.body) uploadedItems.value.push(response.body)
})
.on("complete", (result) => {
if (props.embedded) {
emit("done", { parentNodeId: parentIdRef.value, items: uploadedItems.value })
uploadedItems.value = []
return
}
router.push({ name: "FileManagerList", params: { node: parentIdRef.value } })
})
.on("error", (err) => {
})
.on("restriction-failed", (file, error) => {
})
.on("upload-success", (_file, response) => {
log("Upload success", response)
if (response?.body) {
onCreated(response.body)
uploadedItems.value.push(response.body)
}
})
.on("complete", () => {
const parentNodeId = parentResourceNodeId.value
log("Upload complete, items:", uploadedItems.value, "parent:", parentNodeId)

// Embedded mode (e.g. editor): notify parent and stay in Vue world
if (props.embedded) {
emit("done", {
parentNodeId,
items: uploadedItems.value,
})
uploadedItems.value = []
return
}

if (fileTypeRef.value !== "certificate") {
uppy.value.use(Webcam)
}
// Standalone file manager: go back to listing
router.push({
name: "FileManagerList",
params: { node: parentNodeId || 0 },
query: { cid, sid, gid },
})
})
.on("error", (err) => {
// Avoid crashing the app on Uppy internal errors
log("Uppy error", err)
})
.on("restriction-failed", (file, error) => {
log("Uppy restriction failed", file?.name, error?.message || error)
})

const initialMeta = {
filetype: fileTypeRef.value,
parentResourceNodeId: String(parentIdRef.value),
parentResourceNode: `/api/resource_nodes/${parentIdRef.value}`,
"resourceNode.parent": String(parentIdRef.value),
resourceLinkList: JSON.stringify([{ gid, sid, cid, visibility: RESOURCE_LINK_PUBLISHED }]),
isUncompressZipEnabled: false,
fileExistsOption: "rename",
}
uppy.value.setMeta(initialMeta)

const initialEndpoint = uppy.value.getPlugin("XHRUpload")?.opts?.endpoint
window.__UPPY_DBG = () => {
try {
const plugin = uppy.value?.getPlugin?.("XHRUpload")
const ep = plugin?.opts?.endpoint
} catch (e) {
log("__UPPY_DBG error:", e)
}
}
// Initial meta – mirrors DocumentUpload but targeting personal_files
uppy.setMeta({
filetype,
parentResourceNodeId: parentResourceNodeId.value,
parentResourceNode: `/api/resource_nodes/${parentResourceNodeId.value}`,
"resourceNode.parent": parentResourceNodeId.value,
resourceLinkList,
isUncompressZipEnabled,
fileExistsOption,
})

// Per-filetype restrictions (same concept as DocumentUpload.vue)
if (filetype === "certificate") {
uppy.setOptions({ restrictions: { allowedFileTypes: [".html"] } })
} else if (filetype === "video") {
uppy.setOptions({ restrictions: { allowedFileTypes: ["video/*"] } })
} else {
uppy.setOptions({ restrictions: { allowedFileTypes: null } })
}

function back() {
// Embedded (editor): close popup and let parent decide
if (props.embedded) {
emit("cancel")
return
}
router.push({ name: "FileManagerList", params: { node: parentIdRef.value || 0 } })

const parentNodeId = parentResourceNodeId.value || 0
router.push({
name: "FileManagerList",
params: { node: parentNodeId },
query: { cid, sid, gid },
})
}

onBeforeUnmount(() => {
try {
log("Closing Uppy instance from Upload.vue")
uppy.close()
} catch (e) {
log("Error while closing Uppy", e)
}
})
</script>
Loading