diff --git a/src/modules/resource/components/resource-page/FilesView.vue b/src/modules/resource/components/resource-page/FilesView.vue index 67d3c2dbe2afb09657df8a6db50a9f9a9fe76d66..b06ac4e1f77155860f4be2be8453abfa679a56da 100644 --- a/src/modules/resource/components/resource-page/FilesView.vue +++ b/src/modules/resource/components/resource-page/FilesView.vue @@ -109,12 +109,12 @@ /> <!-- Icon --> <b-icon - :icon="row.item.isFolder ? 'folder' : 'file-earmark'" + :icon="row.item.type === 'Tree' ? 'folder' : 'file-earmark'" class="ml-1" /> </span> <a - v-if="!editableDataUrl || row.item.isFolder" + v-if="!editableDataUrl || row.item.type === 'Tree'" class="fileViewEntry" @click="triggerNavigation(row.item)" > @@ -139,7 +139,7 @@ <!-- Option "Download" --> <b-dropdown-item - v-if="!editableDataUrl && !row.item.isFolder" + v-if="!editableDataUrl && row.item.type === 'Leaf'" @click="openFile(row.item)" > {{ $t("page.resource.metadataManagerBtnDownload") }} diff --git a/src/modules/resource/components/resource-page/MetadataManager.vue b/src/modules/resource/components/resource-page/MetadataManager.vue index 02d741ce21b073da074a34792e215df47c25e56e..d30995f9de1eeaf15809563f91e255c610cf8140 100644 --- a/src/modules/resource/components/resource-page/MetadataManager.vue +++ b/src/modules/resource/components/resource-page/MetadataManager.vue @@ -13,6 +13,7 @@ @downloadExtractedMetadata="downloadExtractedMetadata" @downloadMetadata="downloadMetadata" @selectFiles="selectFiles" + @selectFolders="selectFolders" @showModalDeleteFolderContents="showModalDeleteFolderContents" /> @@ -248,6 +249,7 @@ export default defineComponent({ emits: { clickFileSelect: () => true, + clickFolderSelect: () => true, emptyFileLists: (_: boolean) => true, isUploading: (_: boolean) => true, navigateTree: () => true, @@ -399,7 +401,8 @@ export default defineComponent({ return ( this.isGuest || this.resource?.archived || - this.currentVersion !== this.currentFolderContent?.latestVersion || + (this.currentFileId !== -1 && + this.currentVersion !== this.currentFolderContent?.latestVersion) || this.loadingVersionMetadata ); }, @@ -441,7 +444,8 @@ export default defineComponent({ !this.valid || this.isUploading || this.currentView === "Extracted" || - this.currentFolderContent?.latestVersion !== this.currentVersion || + (this.currentFileId !== -1 && + this.currentVersion !== this.currentFolderContent?.latestVersion) || (this.currentFolderContent?.type === "Leaf" && ((this.editableDataUrl && !this.currentFolderContent.dataUrl) || (this.editableKey && !this.currentFolderContent.name))) @@ -737,7 +741,7 @@ export default defineComponent({ for (const currentFile of this.shownFiles) { if ( (this.uploadDuplicates || - this.folderContents.find((x) => x.name === currentFile.name) === + this.folderContents.find((x) => x.path === currentFile.path) === undefined) && currentFile.type === "Leaf" ) { @@ -777,7 +781,7 @@ export default defineComponent({ this.uploadFileListReplaceFiles = []; for (const fileToUpload of this.fileListUpload) { if ( - this.folderContents.find((x) => x.name === fileToUpload.name) === + this.folderContents.find((x) => x.path === fileToUpload.path) === undefined ) { this.uploadFileListNewFiles.push(fileToUpload); @@ -901,7 +905,7 @@ export default defineComponent({ async handleUploadContent(contents: File, file: FileInformation) { if (this.project?.id && this.resource?.id) { const fileExists = this.folderContents.some( - (x) => x.name === file.name, + (x) => x.path === file.path, ); this.progressStatus = 0; @@ -936,7 +940,7 @@ export default defineComponent({ } // Update or emit the updated folder contents - const entry = this.folderContents.find((x) => x.name === file.name); + const entry = this.folderContents.find((x) => x.path === file.path); if (entry === undefined) { const identifier = uuidv4(); const version = +new Date(); @@ -1036,6 +1040,13 @@ export default defineComponent({ this.$emit("clickFileSelect"); }, + /** + * Emits an event signaling the selection of folder. + */ + selectFolders() { + this.$emit("clickFolderSelect"); + }, + /** * Retrieves the current extracted metadata. * @@ -1139,7 +1150,7 @@ export default defineComponent({ } // Find the corresponding file entry from folder contents - const tmp = this.folderContents.find((x) => x.name === file.name); + const tmp = this.folderContents.find((x) => x.path === file.path); // If the file has a data URL and is editable, handle its content upload if ( diff --git a/src/modules/resource/components/resource-page/metadata/MetadataManagerHeader.vue b/src/modules/resource/components/resource-page/metadata/MetadataManagerHeader.vue index 757a7566a940bb9ab25f9acbf4fb82f42f12f25a..5cf1a4b80aaacece792a63af55cf39cb372d592a 100644 --- a/src/modules/resource/components/resource-page/metadata/MetadataManagerHeader.vue +++ b/src/modules/resource/components/resource-page/metadata/MetadataManagerHeader.vue @@ -1,8 +1,22 @@ <template> <b-row id="metadataManagerButtonRowTop" align-h="between"> <b-col> - <!-- Select Files Button --> - <span id="buttonSelectFilesWrapper"> + <!-- Input Group with Upload button --> + <b-input-group id="metadataManagerUploadMenu"> + <b-dropdown + id="buttonSelectFoldersWrapper" + size="sm" + left + :disabled="uploadDisabled || editableDataUrl" + > + <b-dropdown-item + id="buttonSelectFolder" + @click="$emit('selectFolders')" + > + {{ $t("page.resource.metadataManagerBtnSelectFolders") }} + </b-dropdown-item> + </b-dropdown> + <!-- Select Files Button --> <b-button v-if="!isGuest" id="buttonSelectFiles" @@ -14,10 +28,19 @@ > {{ $t("page.resource.metadataManagerBtnSelectFiles") }} </b-button> - </span> + </b-input-group> + <b-tooltip + v-if="uploadDisabled && uploadDisabledReason" + target="metadataManagerUploadMenu" + triggers="hover" + placement="bottom" + boundary="viewport" + > + {{ uploadDisabledReason }} + </b-tooltip> <b-tooltip v-if="uploadDisabled && uploadDisabledReason" - target="buttonSelectFilesWrapper" + target="buttonSelectFoldersWrapper" triggers="hover" placement="bottom" boundary="viewport" @@ -201,6 +224,13 @@ export default defineComponent({ #metadataManagerButtonRowTop { margin-bottom: 5px; } +#metadataManagerUploadMenu { + margin-right: 5px; + width: auto; +} +#metadataManagerUploadMenu button { + border-radius: 0px; +} #metadataManagerDropDownMenu { margin-right: 5px; width: auto; diff --git a/src/modules/resource/components/resource-page/metadata/MetadataManagerTable.vue b/src/modules/resource/components/resource-page/metadata/MetadataManagerTable.vue index 33271c714d6f2f8df4c36f65717eeb35d61fe4a8..60906b0746cf2054044f00ad67d282fb41905a25 100644 --- a/src/modules/resource/components/resource-page/metadata/MetadataManagerTable.vue +++ b/src/modules/resource/components/resource-page/metadata/MetadataManagerTable.vue @@ -17,7 +17,7 @@ :pressed="false" variant="outline-secondary" @click="$emit('changeMetadata', index)" - >{{ item.name + >{{ item.path }}<b-spinner v-show="item.type === 'Leaf' && item.uploading" label="Spinning" diff --git a/src/modules/resource/i18n/de.ts b/src/modules/resource/i18n/de.ts index 129e71d42bbf44794ffed0461cc336da2937f00d..bba463fea36aecb2aad3ec4140cdfc01ef74c8b1 100644 --- a/src/modules/resource/i18n/de.ts +++ b/src/modules/resource/i18n/de.ts @@ -127,6 +127,7 @@ export default { metadataManagerBtnUpload: "Hochladen", metadataManagerBtnSelectFiles: "Dateien auswählen", + metadataManagerBtnSelectFolders: "Ordner auswählen", metadataManagerBtnCantUploadArchived: "Uploads sind deaktiviert, weil die aktuelle Ressource archiviert ist.", @@ -441,6 +442,7 @@ export default { metadataManagerBtnDownload: "Öffnen", metadataManagerBtnUpload: "Speichern", metadataManagerBtnSelectFiles: "Neuer Eintrag", + metadataManagerBtnSelectFolders: "Neuer Ordner", infoFileType: "Eintrag", infoFileTypeFolder: "Ordner", diff --git a/src/modules/resource/i18n/en.ts b/src/modules/resource/i18n/en.ts index ed40b03d260f9c79d01c8f22e5b3394db9a20e61..eb4499009c9d7bda757a802ceda68ca753ea4def 100644 --- a/src/modules/resource/i18n/en.ts +++ b/src/modules/resource/i18n/en.ts @@ -125,6 +125,7 @@ export default { metadataManagerBtnUpload: "Upload", metadataManagerBtnSelectFiles: "Select Files", + metadataManagerBtnSelectFolders: "Select Folders", metadataManagerBtnCantUploadArchived: "Upload disabled because the current resource is archived.", @@ -431,6 +432,7 @@ export default { metadataManagerBtnDownload: "Open", metadataManagerBtnUpload: "Save", metadataManagerBtnSelectFiles: "New Entry", + metadataManagerBtnSelectFolders: "New folder", infoFileType: "Entry Type", infoFileTypeFolder: "Folder", diff --git a/src/modules/resource/pages/ResourcePage.vue b/src/modules/resource/pages/ResourcePage.vue index c5f4f4cae7b70cc7d0d0766bd6cdda51f7ea00a6..ed3fe7576f66d09567fe9d3b47c2388aba90e866 100644 --- a/src/modules/resource/pages/ResourcePage.vue +++ b/src/modules/resource/pages/ResourcePage.vue @@ -29,6 +29,17 @@ @input="fileListUploadSelected" /> + <!-- Form File Window --> + <b-form-file + ref="folderTrigger" + multiple + class="mt-3" + plain + directory + no-traverse + @input="fileListUploadSelected" + /> + <!-- Files View Column --> <span id="filesViewSpan"> <div @@ -90,6 +101,7 @@ @isUploading="setIsUploading" @showModalDelete="showModalDelete" @clickFileSelect="clickFileSelect" + @clickFolderSelect="clickFolderSelect" /> <!-- Toggle Fullscreen Button --> @@ -141,6 +153,7 @@ import MetadataManager from "../components/resource-page/MetadataManager.vue"; import OversizedFilesModal from "../components/resource-page/modals/OversizedFilesModal.vue"; import DeleteFolderContentsModal from "../components/resource-page/modals/DeleteFolderContentsModal.vue"; import { v4 as uuidv4 } from "uuid"; +import { type ExtendedFile, FileUtil } from "@/modules/resource/utils/FileUtil"; import factory from "rdf-ext"; import type { BFormFile, BTable } from "bootstrap-vue"; import type { Dataset } from "@rdfjs/types"; @@ -277,6 +290,29 @@ export default defineComponent({ } }, + /** + * Trigger folder selection, if not editable data URL is provided. + */ + clickFolderSelect() { + this.showDetail = false; + // Check if editableDataUrl is present + if ( + this.resourceTypeInformation?.resourceContent?.metadataView + ?.editableDataUrl + ) { + this.emptyFileLists(); + } else { + // Trigger a click event on fileTrigger element + (this.$refs.folderTrigger as BFormFile).$el.dispatchEvent( + new MouseEvent("click", { + view: window, + bubbles: true, + cancelable: true, + }), + ); + } + }, + /** * Display the modal for file deletion and set the files to be deleted. * @param {FolderContent[]} files - Files to be deleted. @@ -356,9 +392,9 @@ export default defineComponent({ /** * Add selected files to the fileListUpload list. - * @param {File[] | File} selectedFiles - Selected files. + * @param {ExtendedFile[] | ExtendedFile} selectedFiles - Selected files. */ - fileListUploadSelected(selectedFiles: File[] | File) { + fileListUploadSelected(selectedFiles: ExtendedFile[] | ExtendedFile) { if (!Array.isArray(selectedFiles)) { selectedFiles = [selectedFiles]; } @@ -369,10 +405,23 @@ export default defineComponent({ const identifier = uuidv4(); const version = +new Date(); + const $path = file.$path ? file.$path : null; + + // Correctly determine the parentDirectory based on multiple inputs + const parentDirectory = $path + ? $path.substring(0, $path.lastIndexOf("/")) + : file.path ?? ""; + + // Correctly determine the path based on multiple inputs (check for .path or $path if .name still needs to be appended) + let path = file.path ?? $path ?? ""; + if (!path.endsWith(file.name)) { + path += file.name; + } + const fileInformation: FileInformation = { id: identifier, - path: this.dirTrail + file.name, - parentDirectory: this.dirTrail, + path: this.dirTrail + path, + parentDirectory: this.dirTrail + parentDirectory, type: "Leaf", uploading: false, info: file, @@ -437,19 +486,17 @@ export default defineComponent({ * Handle file drop action for upload. * @param {DragEvent} ev - Drag event. */ - uploadDrop(ev: DragEvent) { + async uploadDrop(ev: DragEvent) { if (this.fileAddable) { this.dragCounter = 0; // Handling file drops if (ev?.dataTransfer?.items) { - for (const item of ev.dataTransfer.items) { - if (item.kind === "file") { - const file = item.getAsFile(); - if (file !== null) { - this.fileListUploadSelected(file); - } - } + const files = await FileUtil.getFilesDataTransferItems( + ev.dataTransfer.items, + ); + for (const file of files) { + this.fileListUploadSelected(file); } } } diff --git a/src/modules/resource/utils/FileUtil.ts b/src/modules/resource/utils/FileUtil.ts index ce46ef3bd4a2d57f35fbe7196428504c7e3ab653..6501e4f82aff8c8ce57aab2bd00aa827d32678fa 100644 --- a/src/modules/resource/utils/FileUtil.ts +++ b/src/modules/resource/utils/FileUtil.ts @@ -4,6 +4,17 @@ import type { QuotaUnit, } from "@coscine/api-client/dist/types/Coscine.Api"; +export interface ExtendedFile extends File { + /** + * From Bootstrap-Vue (is the full path) + */ + $path?: string; + /** + * Custom (is the folder path) + */ + path?: string; +} + export class FileUtil { public static formatBytes(bytes: number, decimals = 2) { if (bytes === 0) { @@ -42,6 +53,52 @@ export class FileUtil { return input.value * Math.pow(k, input_exponent - output_exponent); } + + public static getFilesDataTransferItems( + dataTransferItems: DataTransferItemList, + ): Promise<ExtendedFile[]> { + function traverseFileTreePromise( + item: FileSystemEntry | null, + path = "", + folder: ExtendedFile[], + ) { + return new Promise((resolve) => { + if (item?.isFile) { + (item as FileSystemFileEntry).file((file) => { + (file as ExtendedFile).path = path || "" + file.name; //save full path + folder.push(file as ExtendedFile); + resolve(file); + }); + } else if (item?.isDirectory) { + const dirReader = (item as FileSystemDirectoryEntry).createReader(); + dirReader.readEntries((entries) => { + const entriesPromises = []; + for (const entr of entries) + entriesPromises.push( + traverseFileTreePromise( + entr, + path || "" + item.name + "/", + folder, + ), + ); + resolve(Promise.all(entriesPromises)); + }); + } + }); + } + + const files: ExtendedFile[] = []; + return new Promise((resolve, _) => { + const entriesPromises = []; + for (const it of dataTransferItems) + entriesPromises.push( + traverseFileTreePromise(it.webkitGetAsEntry(), "", files), + ); + Promise.all(entriesPromises).then((_) => { + resolve(files); + }); + }); + } } /**@deprecated Unfortunately that is a workaround, since we can't directly use the QuotaUnit Enum */