diff --git a/src/components/specific/files/all-files-table/AllFilesTable.scss b/src/components/specific/files/all-files-table/AllFilesTable.scss index 5b830aed3..e65206937 100644 --- a/src/components/specific/files/all-files-table/AllFilesTable.scss +++ b/src/components/specific/files/all-files-table/AllFilesTable.scss @@ -60,6 +60,7 @@ justify-content: flex-start; } + &__type, &__created-by, &__tags { position: relative; diff --git a/src/components/specific/files/all-files-table/AllFilesTable.vue b/src/components/specific/files/all-files-table/AllFilesTable.vue index 6d9b0e4bd..026264c40 100644 --- a/src/components/specific/files/all-files-table/AllFilesTable.vue +++ b/src/components/specific/files/all-files-table/AllFilesTable.vue @@ -7,17 +7,36 @@ @update:modelValue="onMainSelectionCheckboxClick" /> -
- {{ $t(columnsDef[0].text) }} - +
+ + {{ $t(columnsDef[0].text) }} + + +
+ + + + + +
{{ $t(columnsDef[1].text) }} @@ -31,10 +50,7 @@ @set-active="activeHeadercolumnKey = $event" />
-
+
{{ $t(columnsDef[2].text) }}
-
+
{{ $t(columnsDef[3].text) }}
-
- {{ $t(columnsDef[4].text) }} +
+ {{ $t(columnsDef[4].text) }}
-
+
{{ $t(columnsDef[5].text) }}
-
+
{{ $t(columnsDef[6].text) }}
-
+
@@ -171,7 +178,7 @@
{{ file.created_by ? `${file.created_by.firstname} ${file.created_by.lastname[0]}.` : "?" @@ -179,14 +186,11 @@
{{ $d(file.updated_at, "long") }}
-
+
-
+
{{ formatBytes(file.size) }}
-
+
@@ -237,7 +235,7 @@ import { computed, reactive, ref, watch } from "vue"; import { useI18n } from "vue-i18n"; import columnsDef, { columnsMD, columnsLG, columnsXL, columnsXXL } from "./columns.js"; import { useStandardBreakpoints } from "../../../../composables/responsive.js"; -import { formatBytes } from "../../../../utils/files.js"; +import { formatBytes, fileExtension } from "../../../../utils/files.js"; import useSortAndFilter from "./sortAndFilter.js"; // Components @@ -301,9 +299,10 @@ export default { const { isMD, isLG, isXL, isXXL } = useStandardBreakpoints(); const filesList = ref(null); + const visibleColumnIds = computed(() => columns.value.map((col) => col.id)); const columns = computed(() => { - let filteredColumns = columnsDef; + let filteredColumns = [...columnsDef]; if (isMD.value) { filteredColumns = columnsMD.map((id) => filteredColumns.find((col) => col.id === id)); } else if (isLG.value) { @@ -318,6 +317,20 @@ export default { })); }); + const formattedAllFiles = computed(() => { + if (!Array.isArray(props.allFiles)) return []; + + return props.allFiles.map((file) => ({ + ...file, + type: + file.nature === "folder" + ? t("t.folder") + : file.name + ? fileExtension(file.name)?.replace(".", "").toUpperCase() + : t("t.file"), + })); + }); + let nameEditMode; watch( () => props.files, @@ -325,7 +338,7 @@ export default { nameEditMode = reactive({}); files.forEach((row) => (nameEditMode[row.id] = false)); }, - { immediate: true } + { immediate: true }, ); const { @@ -338,7 +351,7 @@ export default { updateFilters, activeHeadercolumnKey, filters, - } = useSortAndFilter(computed(() => props.allFiles)); + } = useSortAndFilter(formattedAllFiles); const onFileSelectionChange = (file) => { let newSelection = null; @@ -354,7 +367,7 @@ export default { const mainSelectionCheckboxValue = computed(() => { if (props.selection.length === 0) { return false; - } else if (props.selection.length === props.allFiles.length) { + } else if (props.selection.length === displayedListFiles.value.length) { return true; } else { return null; @@ -363,7 +376,7 @@ export default { const onMainSelectionCheckboxClick = (value) => { let newSelection = null; if (value) { - newSelection = props.allFiles; + newSelection = [...displayedListFiles.value]; } else { newSelection = []; } @@ -376,6 +389,8 @@ export default { columns, mainSelectionCheckboxValue, nameEditMode, + formattedAllFiles, + visibleColumnIds, // Methods formatBytes, updateFilters, @@ -383,7 +398,6 @@ export default { // List filesList, sortObject, - mainSelectionCheckboxValue, onMainSelectionCheckboxClick, activeHeadercolumnKey, toggleSorting, diff --git a/src/components/specific/files/all-files-table/columns.js b/src/components/specific/files/all-files-table/columns.js index ccd3cf04b..b498169ac 100644 --- a/src/components/specific/files/all-files-table/columns.js +++ b/src/components/specific/files/all-files-table/columns.js @@ -1,4 +1,3 @@ -import { fileExtension } from "../../../../utils/files.js"; import i18n from "../../../../i18n/index.js"; import { fullName } from "../../../../utils/users.js"; @@ -8,20 +7,7 @@ export default [ { id: "type", text: "t.type", - sortFunction: (a, b) => { - const getFileType = (file) => fileExtension(file.name); - - const fileTypeA = getFileType(a); - const fileTypeB = getFileType(b); - - if (fileTypeA < fileTypeB) { - return 1; - } else if (fileTypeA > fileTypeB) { - return -1; - } else { - return 0; - } - } + filter: true, }, { id: "name", @@ -31,7 +17,7 @@ export default [ id: "created_by", text: "t.createdBy", filter: true, - filterFunction: rowData => rowData ? fullName(rowData) : t("t.notSpecified"), + filterFunction: (rowData) => (rowData ? fullName(rowData) : t("t.notSpecified")), }, { id: "lastupdate", @@ -52,7 +38,7 @@ export default [ id: "size", text: "t.size", sortable: true, - defaultSortOrder: "asc" + defaultSortOrder: "asc", }, { id: "tags", @@ -63,7 +49,7 @@ export default [ { id: "actions", label: " ", - } + }, ]; export const columnsXXL = ["type", "name", "created_by", "lastupdate", "size", "actions"]; diff --git a/src/components/specific/files/files-manager/FilesManager.vue b/src/components/specific/files/files-manager/FilesManager.vue index 6350447c1..801e6c04d 100644 --- a/src/components/specific/files/files-manager/FilesManager.vue +++ b/src/components/specific/files/files-manager/FilesManager.vue @@ -37,10 +37,14 @@ :fileStructure="fileStructure" :files="selection" :initialFolder="currentFolder" + :loadingFileIds="loadingFileIds" @delete-files="openFileDeleteModal" @delete-visas="openVisaDeleteModal" @download="downloadFiles" @move="moveFiles" + @create-models="createModelFromFiles" + @create-photospheres="createModelFromFiles($event, MODEL_TYPE.PHOTOSPHERE)" + @remove-models="removeModels" /> @@ -75,6 +79,8 @@ @back-parent-folder="backToParent" @create-model="createModelFromFile" @create-photosphere="createModelFromFile($event, MODEL_TYPE.PHOTOSPHERE)" + @create-models="createModelFromFiles" + @create-photospheres="createModelFromFiles($event, MODEL_TYPE.PHOTOSPHERE)" @delete="openFileDeleteModal([$event])" @download="downloadFiles([$event])" @dragover.prevent="() => {}" @@ -338,7 +344,7 @@ export default { filesToUpload.value = files; foldersToUpload.value = await Promise.all( - folders.map((f) => FileService.createFolderStructure(props.project, folder, f)) + folders.map((f) => FileService.createFolderStructure(props.project, folder, f)), ); setTimeout(() => { @@ -348,7 +354,6 @@ export default { }; const loadingFileIds = ref([]); - const createModelFromFile = async (file, type) => { try { loadingFileIds.value.push(file.id); @@ -368,6 +373,41 @@ export default { loadingFileIds.value = loadingFileIds.value.filter((id) => id !== file.id); } }; + const createModelFromFiles = async (files, type) => { + if (!files?.length) return; + try { + selection.value = selection.value.map((f) => { + if (files.includes(f)) return { ...f, nature: "Model" }; + return f; + }); + + loadingFileIds.value.push(...files.map((f) => f.id)); + + const createdModels = await Promise.all( + files.map((file) => + type === MODEL_TYPE.PHOTOSPHERE + ? createPhotosphere(props.project, file) + : createModel(props.project, file), + ), + ); + + createdModels.forEach((model) => { + emit("model-created", model); + }); + + pushNotification({ + type: "success", + title: t("t.success"), + message: + type === MODEL_TYPE.PHOTOSPHERE + ? t("FilesManager.createPhotospheresNotification") + : t("FilesManager.createModelsNotification"), + }); + } finally { + const ids = files.map((f) => f.id); + loadingFileIds.value = loadingFileIds.value.filter((id) => !ids.includes(id)); + } + }; const removeModel = async (file) => { try { @@ -378,6 +418,37 @@ export default { } }; + const removeModels = async (files) => { + if (!files?.length) return; + + try { + selection.value = selection.value.map((f) => { + if (files.includes(f)) return { ...f, nature: "Document" }; + return f; + }); + loadingFileIds.value.push(...files.map((f) => f.id)); + + const modelsToDelete = files + .filter((file) => file.model_id && file.model_type) + .map((file) => ({ + id: file.model_id, + type: file.model_type, + })); + + if (!modelsToDelete.length) return; + + await deleteModels(props.project, modelsToDelete); + pushNotification({ + type: "success", + title: t("t.success"), + message: t("FilesManager.removeModelsNotification"), + }); + } finally { + const ids = files.map((f) => f.id); + loadingFileIds.value = loadingFileIds.value.filter((id) => !ids.includes(id)); + } + }; + const filesToDelete = ref([]); const showDeleteModal = ref(false); const openFileDeleteModal = (files) => { @@ -465,7 +536,7 @@ export default { } else { closeAccessManager(); } - } + }, ); }; const closeAccessManager = () => { @@ -543,7 +614,7 @@ export default { createdVisas.value = createdResponse; if (route.query.visaId) { currentVisa.value = toValidateVisas.value.find( - (v) => v.id === parseInt(route.query.visaId) + (v) => v.id === parseInt(route.query.visaId), ); if (currentVisa.value) { openVisaManager(currentVisa.value); @@ -554,7 +625,7 @@ export default { const visasCounter = computed( () => toValidateVisas.value.filter((v) => v.status !== VISA_STATUS.CLOSE).length + - createdVisas.value.filter((v) => v.status !== VISA_STATUS.CLOSE).length + createdVisas.value.filter((v) => v.status !== VISA_STATUS.CLOSE).length, ); const fetchTags = async () => { @@ -660,19 +731,22 @@ export default { const searchText = ref(""); const { filteredList: displayedFiles, searchText: filterFilesSearchText } = useListFilter( currentFiles, - (file) => file.name + (file) => file.name, ); const { filteredList: displayedAllFiles, searchText: filterAllFilesSearchText } = useListFilter( allFiles, - (file) => file.name + (file) => file.name, ); const { filteredList: displayedVisas, searchText: filterVisasSearchText } = useListFilter( allVisas, - (visa) => visa.document.name + (visa) => visa.document.name, ); - const jumpToTargetFolder = (folderId) => { - selectedFileTab.value = filesTabs[0]; + const jumpToTargetFolder = (folderId, forceFoldersTab = false) => { + if (forceFoldersTab) { + selectedFileTab.value = filesTabs[0]; + } + const folder = handler.get({ nature: FILE_TYPE.FOLDER, id: folderId }); currentFolder.value = handler.deserialize(folder); }; @@ -695,7 +769,7 @@ export default { currentFolder.value = struct; } }, - { immediate: true } + { immediate: true }, ); watch( @@ -706,7 +780,7 @@ export default { currentFiles.value = childrenFolders.concat(childrenFiles); gedTargetFolder.set(folder.id); }, - { immediate: true } + { immediate: true }, ); return { @@ -749,6 +823,7 @@ export default { closeVersioningManager, closeVisaManager, createModelFromFile, + createModelFromFiles, downloadFiles, fetchTags, fetchVisas, @@ -768,6 +843,7 @@ export default { openVersioningManager, openVisaManager, removeModel, + removeModels, setSelection, uploadFiles, visasLoading, diff --git a/src/components/specific/files/files-manager/files-action-bar/FilesActionBar.vue b/src/components/specific/files/files-manager/files-action-bar/FilesActionBar.vue index 1ed539fbe..34db52f16 100644 --- a/src/components/specific/files/files-manager/files-action-bar/FilesActionBar.vue +++ b/src/components/specific/files/files-manager/files-action-bar/FilesActionBar.vue @@ -34,6 +34,45 @@ {{ $t("t.download") }} + + + + diff --git a/src/components/specific/files/folder-table/FoldersTable.vue b/src/components/specific/files/folder-table/FoldersTable.vue index 19cc8f3b9..76c4e7a55 100644 --- a/src/components/specific/files/folder-table/FoldersTable.vue +++ b/src/components/specific/files/folder-table/FoldersTable.vue @@ -5,7 +5,7 @@ data-test-id="files-table" tableLayout="fixed" :columns="columns" - :rows="files" + :rows="formattedFiles" rowKey="id" :rowHeight="48" :selectable="true" @@ -112,7 +112,7 @@ import { useI18n } from "vue-i18n"; import columnsDef, { columnsLG, columnsXL } from "./columns.js"; import { useStandardBreakpoints } from "../../../../composables/responsive.js"; import { isFolder } from "../../../../utils/file-structure.js"; -import { formatBytes, generateFileKey } from "../../../../utils/files.js"; +import { formatBytes, generateFileKey, fileExtension } from "../../../../utils/files.js"; // Components import FilesManagerBreadcrumb from "../files-manager/files-manager-breadcrumb/FilesManagerBreadcrumb.vue"; @@ -132,7 +132,7 @@ export default { FileTagsCell, FileTypeCell, FileUploadCard, - FilesManagerBreadcrumb + FilesManagerBreadcrumb, }, props: { selection: { @@ -198,6 +198,20 @@ export default { })); }); + const formatExtension = (name) => { + const ext = fileExtension(name); + + if (!ext) return t("t.file"); + + return ext.replace(".", "").toUpperCase(); + }; + const formattedFiles = computed(() => + props.files.map((file) => ({ + ...file, + type: isFolder(file) ? t("t.folder") : file.name ? formatExtension(file.name) : t("t.file"), + })), + ); + const onRowDrop = ({ event, data }) => { event.preventDefault(); event.stopPropagation(); @@ -211,7 +225,7 @@ export default { nameEditMode = reactive({}); files.forEach((row) => (nameEditMode[row.id] = false)); }, - { immediate: true } + { immediate: true }, ); const fileUploads = ref([]); @@ -219,9 +233,9 @@ export default { () => props.filesToUpload, (files) => { fileUploads.value = fileUploads.value.concat( - files.map((f) => Object.assign(f, { key: generateFileKey(f) })) + files.map((f) => Object.assign(f, { key: generateFileKey(f) })), ); - } + }, ); const folderUploads = ref([]); @@ -229,9 +243,9 @@ export default { () => props.foldersToUpload, (folders) => { folderUploads.value = folderUploads.value.concat( - folders.map((f) => Object.assign(f, { key: generateFileKey(f) })) + folders.map((f) => Object.assign(f, { key: generateFileKey(f) })), ); - } + }, ); const onUploadCompleted = (key, file) => { @@ -260,6 +274,7 @@ export default { filesTable, fileUploads, folderUploads, + formattedFiles, nameEditMode, // Methods cleanUpload, @@ -267,7 +282,7 @@ export default { isFolder, onRowDrop, onUploadCompleted, - } - } -} + }; + }, +}; diff --git a/src/components/specific/files/folder-table/columns.js b/src/components/specific/files/folder-table/columns.js index 9159cc3bf..fa2e1041e 100644 --- a/src/components/specific/files/folder-table/columns.js +++ b/src/components/specific/files/folder-table/columns.js @@ -1,5 +1,4 @@ import i18n from "../../../../i18n/index.js"; -import { fileExtension } from "../../../../utils/files.js"; const { t } = i18n.global; @@ -9,34 +8,13 @@ export default [ text: "t.type", width: "80px", align: "center", - sortable: true, - defaultSortOrder: "asc", - sortFunction: (a, b) => { - const getFileType = (file) => { - if (file.nature === 'folder') { - return 'Folder'; - } else { - return fileExtension(file.name); - } - }; - - const fileTypeA = getFileType(a); - const fileTypeB = getFileType(b); - - if (fileTypeA < fileTypeB) { - return 1; - } else if (fileTypeA > fileTypeB) { - return -1; - } else { - return 0; - } - } + filter: true, }, { id: "name", text: "t.name", sortable: true, - defaultSortOrder: "desc" + defaultSortOrder: "desc", }, { id: "created_by", @@ -44,7 +22,8 @@ export default [ width: "160px", align: "center", filter: true, - filterFunction: rowData => rowData ? `${rowData.lastname} ${rowData.firstname}` : t("t.notSpecified"), + filterFunction: (rowData) => + rowData ? `${rowData.lastname} ${rowData.firstname}` : t("t.notSpecified"), }, { id: "lastupdate", @@ -65,7 +44,7 @@ export default [ width: "100px", align: "center", sortable: true, - defaultSortOrder: "asc" + defaultSortOrder: "asc", }, { id: "tags", @@ -80,7 +59,7 @@ export default [ label: " ", width: "50px", align: "center", - } + }, ]; export const columnsXL = ["name", "lastupdate", "size", "actions"]; diff --git a/src/i18n/lang/en.json b/src/i18n/lang/en.json index 63d5ac73e..71edfee9e 100644 --- a/src/i18n/lang/en.json +++ b/src/i18n/lang/en.json @@ -130,6 +130,10 @@ "previewModelButtonText": "Preview", "createPhotosphereButtonText": "Define as photosphere" }, + "FileNameCell": { + "modelFile": "Model", + "photosphereFile": "Photosphere" + }, "FilesActionBar": { "moveButtonText": "Move to" }, @@ -141,7 +145,10 @@ "FilesManager": { "title": "Project Files", "createModelNotification": "Model successfully created", + "createModelsNotification": "Models successfully created", + "createPhotospheresNotification": "Photospheres successfully created", "structureImport": "Import a GED structure", + "removeModelsNotification": "Models successfully removed", "gedDownload": "Download the GED", "folderImport": "Import a folder", "foldersTab": "Folders", diff --git a/src/i18n/lang/fr.json b/src/i18n/lang/fr.json index 42dc966f4..86a73d8d9 100644 --- a/src/i18n/lang/fr.json +++ b/src/i18n/lang/fr.json @@ -192,6 +192,10 @@ "versioningButtonText": "Ajouter une version", "visaButtonText": "Demande de validation" }, + "FileNameCell": { + "modelFile": "Modèle", + "photosphereFile": "Photosphère" + }, "FilesActionBar": { "moveButtonText": "Déplacer vers" }, @@ -203,8 +207,11 @@ "FilesManager": { "title": "Documents du projet", "createModelNotification": "Modèle créé avec succès", + "createModelsNotification": "Modèles créés avec succès", "createPhotosphereNotification": "Photosphère créée avec succès", + "createPhotospheresNotification": "Photosphères créées avec succès", "structureImport": "Importer une structure GED", + "removeModelsNotification": "Modèles retirés avec succès", "gedDownload": "Télécharger la GED", "folderImport": "Importer un dossier", "foldersTab": "Dossiers",