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..fefa3e28d 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 = formattedAllFiles.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 509abb930..f433074dc 100644 --- a/src/components/specific/files/files-manager/FilesManager.vue +++ b/src/components/specific/files/files-manager/FilesManager.vue @@ -41,6 +41,9 @@ @delete-visas="openVisaDeleteModal" @download="downloadFiles" @move="moveFiles" + @create-models="createModelFromFiles" + @create-photospheres="createModelFromFiles($event, MODEL_TYPE.PHOTOSPHERE)" + @remove-models="removeModels" /> @@ -75,6 +78,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="() => {}" @@ -339,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(() => { @@ -370,6 +375,35 @@ export default { } }; + const createModelFromFiles = async (files, type) => { + if (!files?.length) return; + + try { + 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: 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 { loadingFileIds.value.push(file.id); @@ -379,6 +413,29 @@ export default { } }; + const removeModels = async (files) => { + console.log({ files }); + if (!files?.length) return; + + try { + 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); + } 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) => { @@ -474,7 +531,7 @@ export default { } else { closeAccessManager(); } - } + }, ); }; const closeAccessManager = () => { @@ -552,7 +609,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); @@ -563,7 +620,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 () => { @@ -669,15 +726,15 @@ 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) => { @@ -704,7 +761,7 @@ export default { currentFolder.value = struct; } }, - { immediate: true } + { immediate: true }, ); watch( @@ -715,7 +772,7 @@ export default { currentFiles.value = childrenFolders.concat(childrenFiles); gedTargetFolder.set(folder.id); }, - { immediate: true } + { immediate: true }, ); return { @@ -758,6 +815,7 @@ export default { closeVersioningManager, closeVisaManager, createModelFromFile, + createModelFromFiles, downloadFiles, fetchTags, fetchVisas, @@ -778,6 +836,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..9c1fab636 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,37 @@ {{ $t("t.download") }} + + + + import { useToggle } from "../../../../../composables/toggle.js"; import { useUser } from "../../../../../state/user.js"; +import { isFolder } from "../../../../../utils/file-structure.js"; +import { isConvertible, isConvertibleToPhotosphere, isModel } from "../../../../../utils/models.js"; // Components import FolderSelector from "../../folder-selector/FolderSelector.vue"; @@ -77,7 +110,15 @@ export default { required: true, }, }, - emits: ["delete-files", "delete-visas", "download", "move"], + emits: [ + "delete-files", + "delete-visas", + "download", + "move", + "create-models", + "create-photospheres", + "remove-models", + ], setup(props, { emit }) { const { hasAdminPerm } = useUser(); @@ -87,14 +128,23 @@ export default { toggle: toggleFolderSelector, } = useToggle(); - const isFilesOrFolder = files => files.some( - f => f.nature === "Document" || f.nature === "Model" || f.nature === "Folder" - ); + const isFilesOrFolder = (files) => + files.some((f) => f.nature === "Document" || f.nature === "Model" || f.nature === "Folder"); - const onDeleteClick = files => { + const onDeleteClick = (files) => { emit(isFilesOrFolder(files) ? "delete-files" : "delete-visas", files); }; + const canConvertAllToModel = (files) => + files.every((file) => !isFolder(file) && isConvertible(file) && !isModel(file)); + + const canConvertAllToPhotosphere = (files) => + files.every((file) => !isFolder(file) && isConvertibleToPhotosphere(file) && !isModel(file)); + + const canRemoveAllModels = (files) => + files.length > 0 && + files.every((file) => !isFolder(file) && isModel(file) && isConvertible(file)); + return { // References showFolderSelector, @@ -104,6 +154,9 @@ export default { isFilesOrFolder, onDeleteClick, toggleFolderSelector, + canConvertAllToModel, + canConvertAllToPhotosphere, + canRemoveAllModels, }; }, }; 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..47e315128 100644 --- a/src/i18n/lang/en.json +++ b/src/i18n/lang/en.json @@ -141,6 +141,7 @@ "FilesManager": { "title": "Project Files", "createModelNotification": "Model successfully created", + "createModelsNotification": "Models successfully created", "structureImport": "Import a GED structure", "gedDownload": "Download the GED", "folderImport": "Import a folder", diff --git a/src/i18n/lang/fr.json b/src/i18n/lang/fr.json index 42dc966f4..40d3d9cfe 100644 --- a/src/i18n/lang/fr.json +++ b/src/i18n/lang/fr.json @@ -203,6 +203,7 @@ "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", "structureImport": "Importer une structure GED", "gedDownload": "Télécharger la GED",