diff --git a/package.json b/package.json
index 4f74f5cc3..9285647c2 100644
--- a/package.json
+++ b/package.json
@@ -48,4 +48,4 @@
"vite": "^8.1.5",
"vite-plugin-environment": "^1.1.3"
}
-}
\ No newline at end of file
+}
diff --git a/src/components/specific/files/all-files-table/AllFilesTable.vue b/src/components/specific/files/all-files-table/AllFilesTable.vue
index cbeca91a1..5064933d4 100644
--- a/src/components/specific/files/all-files-table/AllFilesTable.vue
+++ b/src/components/specific/files/all-files-table/AllFilesTable.vue
@@ -218,6 +218,7 @@
@download="$emit('download', file)"
@file-clicked="$emit('file-clicked', file)"
@manage-access="$emit('manage-access', file)"
+ @manage-naming-rule="$emit('manage-naming-rule', file)"
@open-tag-manager="$emit('open-tag-manager', file)"
@open-versioning-manager="$emit('open-versioning-manager', file)"
@open-visa-manager="$emit('open-visa-manager', file)"
@@ -288,6 +289,7 @@ export default {
"file-clicked",
"go-folders-view",
"manage-access",
+ "manage-naming-rule",
"open-tag-manager",
"open-versioning-manager",
"open-visa-manager",
diff --git a/src/components/specific/files/files-manager/FilesManager.vue b/src/components/specific/files/files-manager/FilesManager.vue
index 243a31375..8a288d8e0 100644
--- a/src/components/specific/files/files-manager/FilesManager.vue
+++ b/src/components/specific/files/files-manager/FilesManager.vue
@@ -11,6 +11,7 @@
:initialSearchText="searchText"
@update:searchText="searchText = $event"
@upload-files="uploadFiles"
+ @manage-naming-conflicts="openNamingConflictsModal"
/>
@@ -76,6 +77,7 @@
:filesToUpload="filesToUpload"
:folder="currentFolder"
:foldersToUpload="foldersToUpload"
+ :hasNamingConflict="hasNamingConflict"
@back-parent-folder="backToParent"
@create-model="createModelFromFile"
@create-photosphere="createModelFromFile($event, MODEL_TYPE.PHOTOSPHERE)"
@@ -88,6 +90,7 @@
@file-clicked="onFileSelected"
@file-uploaded="$emit('file-uploaded')"
@manage-access="openAccessManager"
+ @manage-naming-rule="openFolderNamingConstraintManager"
@open-tag-manager="openTagManager"
@open-versioning-manager="openVersioningManager"
@open-visa-manager="openVisaManager"
@@ -111,6 +114,7 @@
@file-clicked="onFileSelected"
@go-folders-view="goFoldersView"
@manage-access="openAccessManager"
+ @manage-naming-rule="openFolderNamingConstraintManager"
@open-tag-manager="openTagManager"
@open-versioning-manager="openVersioningManager"
@open-visa-manager="openVisaManager"
@@ -205,12 +209,14 @@ import FileService from "../../../../services/FileService.js";
import TagService from "../../../../services/TagService";
import { useFiles } from "../../../../state/files.js";
import { useModels } from "../../../../state/models.js";
+import { useNamingConstraints } from "../../../../state/naming-constraints.js";
import { useProjects } from "../../../../state/projects.js";
import { useSpaces } from "../../../../state/spaces.js";
import { useVisa } from "../../../../state/visa.js";
import { collectDescendants } from "../../../../utils/file-tree.js";
import { isFolder } from "../../../../utils/file-structure.js";
import { getFilesFromEvent } from "../../../../utils/files.js";
+import { matchName } from "../../../../utils/naming-constraint.js";
import { isFullTotal } from "../../../../utils/spaces.js";
import { fileUploadInput } from "../../../../utils/upload.js";
@@ -225,6 +231,8 @@ import FilesManagerOnboarding from "./files-manager-onboarding/FilesManagerOnboa
import FileTree from "../file-tree/FileTree.vue";
import FileTreePreviewModal from "../file-tree-preview-modal/FileTreePreviewModal.vue";
import FolderAccessManager from "../folder-access-manager/FolderAccessManager.vue";
+import FolderNamingConstraintManager from "../naming-constraint/FolderNamingConstraintManager.vue";
+import NamingConflictModal from "../naming-constraint/NamingConflictModal.vue";
import FoldersTable from "../folder-table/FoldersTable.vue";
import SubscriptionModal from "../../subscriptions/subscription-modal/SubscriptionModal.vue";
import TagsMain from "../../tags/tags-main/TagsMain.vue";
@@ -264,6 +272,10 @@ export default {
type: Object,
required: true,
},
+ refreshFiles: {
+ type: Function,
+ required: true,
+ },
},
emits: ["file-uploaded", "file-updated", "model-created"],
setup(props, { emit }) {
@@ -280,7 +292,7 @@ export default {
const { createModel, createPhotosphere, deleteModels } = useModels();
const { fetchToValidateVisas, fetchCreatedVisas } = useVisa();
-
+ const { getEffectiveFolderRule } = useNamingConstraints();
const currentFolder = ref(null);
const currentFiles = ref([]);
const toValidateVisas = ref([]);
@@ -337,20 +349,55 @@ export default {
const filesToUpload = ref([]);
const foldersToUpload = ref([]);
- const uploadFiles = async (event, folder = currentFolder.value) => {
- const { files, folders } = await getFilesFromEvent(event);
+ const proceedUpload = async ({ files, folders }, folder) => {
files.forEach((file) => (file.folder = folder));
-
filesToUpload.value = files;
foldersToUpload.value = await Promise.all(
folders.map((f) => FileService.createFolderStructure(props.project, folder, f)),
);
-
setTimeout(() => {
filesToUpload.value = [];
foldersToUpload.value = [];
}, 10);
};
+ let folders = [];
+ const uploadFiles = async (event, folder = currentFolder.value) => {
+ const { files, folders } = await getFilesFromEvent(event);
+
+ const rule = await getEffectiveFolderRule(props.project, folder);
+ const invalidFiles = rule?.rule
+ ? files.filter((file) => !matchName(file.name, rule.rule))
+ : [];
+
+ if (invalidFiles.length > 0) {
+ openModal({
+ component: NamingConflictModal,
+ props: {
+ project: props.project,
+ documents: invalidFiles.map((file, i) => ({ id: `upload-${i}`, name: file.name })),
+ rule,
+ persistChanges: false,
+ onClose: closeModal,
+ onConfirm: ({ renamed, deleted }) => {
+ const deletedIds = new Set(deleted.map((d) => d.id));
+ const renamedById = new Map(renamed.map((r) => [r.id, r.name]));
+ const finalFiles = invalidFiles
+ .map((file, i) => ({ file, id: `upload-${i}` }))
+ .filter(({ id }) => !deletedIds.has(id))
+ .map(({ file, id }) => {
+ const name = renamedById.get(id);
+ return name ? new File([file], name, { type: file.type }) : file;
+ });
+ const validFiles = files.filter((file) => matchName(file.name, rule.rule));
+ proceedUpload({ files: [...validFiles, ...finalFiles], folders }, folder);
+ },
+ },
+ });
+ return;
+ }
+
+ proceedUpload({ files, folders }, folder);
+ };
const loadingFileIds = ref([]);
const isCreatingModels = ref(false);
@@ -544,6 +591,49 @@ export default {
}, 100);
};
+ const openFolderNamingConstraintManager = (folder) => {
+ openSidePanel("right", {
+ component: FolderNamingConstraintManager,
+ props: {
+ project: props.project,
+ folder,
+ refreshFiles: props.refreshFiles,
+ },
+ });
+ };
+
+ const openNamingConflictsModal = async () => {
+ const conflicting = allFiles.value.filter((file) => file.naming_constraint_conflict);
+ if (conflicting.length === 0) {
+ pushNotification({
+ type: "success",
+ title: t("NamingConstraint.noConflictsTitle"),
+ message: t("NamingConstraint.noConflictsMessage"),
+ });
+ return;
+ }
+ const documents = await Promise.all(
+ conflicting.map(async (file) => {
+ const folder = allFolders.value.find((f) => f.id === file.parent_id);
+ const effective = folder ? await getEffectiveFolderRule(props.project, folder) : null;
+ return { ...file, namingRule: effective?.rule ?? null };
+ }),
+ );
+ openModal({
+ component: NamingConflictModal,
+ props: {
+ project: props.project,
+ documents,
+ allFolders: allFolders.value,
+ rule: null,
+ onClose: closeModal,
+ onConfirm: async () => {
+ await props.refreshFiles();
+ },
+ },
+ });
+ };
+
const visasLoading = ref(false);
const openVisaManager = (file) => {
onTabChange(filesTabs[2]);
@@ -683,6 +773,19 @@ export default {
const allFiles = computed(() => getFilesInFolder(props.fileStructure));
const allFolders = computed(() => getFoldersInFolder(props.fileStructure));
+ const hasNamingConflict = (folder) => {
+ if (!folder?.children?.length) {
+ return false;
+ }
+
+ return folder.children.some((child) => {
+ if (isFolder(child)) {
+ return hasNamingConflict(child);
+ }
+ return child.naming_constraint_conflict;
+ });
+ };
+
const filesTabs = [
{
id: "folders",
@@ -829,10 +932,13 @@ export default {
fileUploadInput,
goFoldersView,
goVisasView,
+ hasNamingConflict,
isFullTotal,
moveFiles,
onFileSelected,
openAccessManager,
+ openFolderNamingConstraintManager,
+ openNamingConflictsModal,
openFileDeleteModal,
openVisaDeleteModal,
openSidePanel,
diff --git a/src/components/specific/files/files-manager/files-manager-actions/FilesManagerActions.vue b/src/components/specific/files/files-manager/files-manager-actions/FilesManagerActions.vue
index f2228ba58..405627f86 100644
--- a/src/components/specific/files/files-manager/files-manager-actions/FilesManagerActions.vue
+++ b/src/components/specific/files/files-manager/files-manager-actions/FilesManagerActions.vue
@@ -42,7 +42,7 @@
$t(
`ProjectOverview.uploadDisableMessage.${
isFullTotal(spaceSubInfo) ? 'size' : 'permission'
- }`
+ }`,
)
"
>
@@ -104,6 +104,24 @@
clear
/>
+
+
+
+
+
+ {{ $t("NamingConstraint.managerTitle") }}
+
+
+
@@ -111,18 +129,23 @@
import { computed, ref, inject, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useAppModal } from "../../../app/app-modal/app-modal.js";
+import { useAppSidePanel } from "../../../../../components/specific/app/app-side-panel/app-side-panel.js";
import {
useStandardBreakpoints,
useCustomBreakpoints,
} from "../../../../../composables/responsive.js";
import { useFiles } from "../../../../../state/files.js";
+import { useProjects } from "../../../../../state/projects.js";
import { useUser } from "../../../../../state/user.js";
import { isFullTotal } from "../../../../../utils/spaces.js";
+import { collectDescendants } from "../../../../../utils/file-tree.js";
+import { isFolder } from "../../../../../utils/file-structure.js";
import { fileUploadInput } from "../../../../../utils/upload.js";
// Components
import FileDragAndDropModal from "../file-drag-and-drop-modal/FileDragAndDropModal.vue";
import FolderCreationButton from "../../folder-creation-button/FolderCreationButton.vue";
+import NamingConstraintsManager from "../../../../../components/specific/files/naming-constraint/NamingConstraintsManager.vue";
export default {
components: {
@@ -157,25 +180,36 @@ export default {
required: true,
},
},
- emits: ["open-subscription-modal", "update:searchText", "upload-files"],
+ emits: [
+ "open-subscription-modal",
+ "update:searchText",
+ "upload-files",
+ "manage-naming-conflicts",
+ ],
setup(props, { emit }) {
const { t } = useI18n();
const { isUserOrga, isProjectAdmin, isProjectGuest, hasAdminPerm } = useUser();
const { openModal } = useAppModal();
+ const { openSidePanel } = useAppSidePanel();
+ const { currentProject } = useProjects();
const shouldSubscribe = inject("shouldSubscribe");
-
- const {
- downloadFiles: download,
- projectFileStructure,
- } = useFiles();
+ const { downloadFiles: download, projectFileStructure } = useFiles();
const downloadFiles = async (files) => {
await download(props.project, files);
};
const dropdown = ref(null);
+ const conflictCount = computed(() => {
+ const root = projectFileStructure.value;
+ if (!root) return 0;
+ return collectDescendants(
+ root,
+ (child) => !isFolder(child) && child.naming_constraint_conflict,
+ ).length;
+ });
const menuItems = computed(() => {
const items = [];
@@ -188,10 +222,22 @@ export default {
{
name: t("FilesManager.gedDownload"),
action: () => downloadFiles([projectFileStructure.value]),
- }
+ },
);
}
+ if (isProjectAdmin(props.project)) {
+ items.push({
+ name:
+ t("NamingConstraint.renameConflictsMenuItem") +
+ (conflictCount.value > 0 ? ` (${conflictCount.value})` : ""),
+ action: () => {
+ emit("manage-naming-conflicts");
+ dropdown.value.displayed = false;
+ },
+ });
+ }
+
if (hasAdminPerm(props.project, props.currentFolder)) {
items.splice(1, 0, {
name: t("FilesManager.folderImport"),
@@ -219,7 +265,7 @@ export default {
return `${fileManager.value?.$el?.getBoundingClientRect().height - H - y}px`;
});
- const { isMD, isLG, isXL } = useStandardBreakpoints();
+ const { isMD, isLG, isXL, isXXL } = useStandardBreakpoints();
const { isMidXXL, isXXXL } = useCustomBreakpoints({
isMidXXL: ({ width }) => width <= 1277 - 0.02,
isXXXL: ({ width }) => width <= 1521 - 0.02,
@@ -228,19 +274,28 @@ export default {
const isLargeLayout = computed(
() =>
(isProjectAdmin(props.project) && !isXXXL.value) ||
- (!isProjectAdmin(props.project) && !isMidXXL.value)
+ (!isProjectAdmin(props.project) && !isMidXXL.value),
);
const isMediumLayout = computed(
() =>
(isProjectAdmin(props.project) && !isXL.value && isXXXL.value) ||
- (!isProjectAdmin(props.project) && !isMD.value && isMidXXL.value)
+ (!isProjectAdmin(props.project) && !isMD.value && isMidXXL.value),
);
- const searchText = ref(props.initialSearchText || '');
+ const searchText = ref(props.initialSearchText || "");
watch(searchText, (newValue) => {
- emit('update:searchText', newValue);
+ emit("update:searchText", newValue);
});
+ const openNamingConstraintsManager = () => {
+ openSidePanel("right", {
+ component: NamingConstraintsManager,
+ props: {
+ project: currentProject.value,
+ },
+ });
+ };
+
return {
// References
dropdown,
@@ -255,11 +310,14 @@ export default {
downloadFiles,
hasAdminPerm,
isFullTotal,
+ isProjectAdmin,
isUserOrga,
fileUploadInput,
+ openNamingConstraintsManager,
// Responsive breakpoints
isMD,
isLG,
+ isXXL,
};
},
};
diff --git a/src/components/specific/files/files-table/file-actions-cell/FileActionsCell.vue b/src/components/specific/files/files-table/file-actions-cell/FileActionsCell.vue
index 132936db2..41f411753 100644
--- a/src/components/specific/files/files-table/file-actions-cell/FileActionsCell.vue
+++ b/src/components/specific/files/files-table/file-actions-cell/FileActionsCell.vue
@@ -14,27 +14,22 @@
-