diff --git a/package-lock.json b/package-lock.json
index 473a908d4..6be98ffef 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,7 +11,7 @@
"@bimdata/bcf-components": "6.7.8",
"@bimdata/components": "1.10.3",
"@bimdata/design-system": "2.4.4",
- "@bimdata/typescript-fetch-api-client": "10.48.0",
+ "@bimdata/typescript-fetch-api-client": "10.48.1",
"@bimdata/viewer": "2.20.2-beta.2",
"@paddle/paddle-js": "^1.6.5",
"async": "^3.2.6",
@@ -1595,9 +1595,9 @@
"integrity": "sha512-S9rpxvwR30ylGeyuqIXGbeZO7oOEBPfwxxYV7xkkT/TkFvjcx1GmzzE2VzoaoM9zhXX8RP5Z3e+XXPFn7fDG1A=="
},
"node_modules/@bimdata/typescript-fetch-api-client": {
- "version": "10.48.0",
- "resolved": "https://registry.npmjs.org/@bimdata/typescript-fetch-api-client/-/typescript-fetch-api-client-10.48.0.tgz",
- "integrity": "sha512-QLMOSAzvPArPxPauRJ/n5Mg6VMdWUWGg5eamngno06svmofzFInj+lkyVIwpwvTBhMGez80cOqCGZXi6kxGlFw=="
+ "version": "10.48.1",
+ "resolved": "https://registry.npmjs.org/@bimdata/typescript-fetch-api-client/-/typescript-fetch-api-client-10.48.1.tgz",
+ "integrity": "sha512-Q/CVJjENfADu2RPdNRjSZH4I1PCrOhprv32P6X4CoO0i4C2RUUfC7b7qj3q94L/ZKSrUZscR7nCMKwvQ2NiKTQ=="
},
"node_modules/@bimdata/viewer": {
"version": "2.20.2-beta.2",
@@ -2868,9 +2868,9 @@
}
},
"node_modules/@napi-rs/wasm-runtime": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.4.tgz",
- "integrity": "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==",
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.5.tgz",
+ "integrity": "sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA==",
"dev": true,
"license": "MIT",
"optional": true,
diff --git a/package.json b/package.json
index b198702ce..9ec70de22 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,7 @@
"@bimdata/bcf-components": "6.7.8",
"@bimdata/components": "1.10.3",
"@bimdata/design-system": "2.4.4",
- "@bimdata/typescript-fetch-api-client": "10.48.0",
+ "@bimdata/typescript-fetch-api-client": "10.48.1",
"@bimdata/viewer": "2.20.2-beta.2",
"@paddle/paddle-js": "^1.6.5",
"async": "^3.2.6",
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..29db8dc3d 100644
--- a/src/components/specific/files/files-manager/FilesManager.vue
+++ b/src/components/specific/files/files-manager/FilesManager.vue
@@ -9,8 +9,10 @@
:spaceSubInfo="spaceSubInfo"
:importFromOtherProjectsActions="importFromOtherProjectsActions"
:initialSearchText="searchText"
+ :refreshFiles="refreshFiles"
@update:searchText="searchText = $event"
@upload-files="uploadFiles"
+ @manage-naming-conflicts="openNamingConflictsModal"
/>
@@ -74,8 +76,9 @@
:loadingFileIds="loadingFileIds"
:selection="selection"
:filesToUpload="filesToUpload"
- :folder="currentFolder"
+ :folder="folderWithNamingPreview"
:foldersToUpload="foldersToUpload"
+ :hasNamingConflict="hasNamingConflict"
@back-parent-folder="backToParent"
@create-model="createModelFromFile"
@create-photosphere="createModelFromFile($event, MODEL_TYPE.PHOTOSPHERE)"
@@ -88,6 +91,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 +115,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 +210,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 +232,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 +273,10 @@ export default {
type: Object,
required: true,
},
+ refreshFiles: {
+ type: Function,
+ required: true,
+ },
},
emits: ["file-uploaded", "file-updated", "model-created"],
setup(props, { emit }) {
@@ -280,8 +293,14 @@ export default {
const { createModel, createPhotosphere, deleteModels } = useModels();
const { fetchToValidateVisas, fetchCreatedVisas } = useVisa();
-
+ const { getEffectiveFolderRule } = useNamingConstraints();
const currentFolder = ref(null);
+ const currentFolderNamingRule = ref(null);
+ const folderWithNamingPreview = computed(() =>
+ currentFolder.value
+ ? { ...currentFolder.value, namingConstraintPreview: currentFolderNamingRule.value }
+ : currentFolder.value,
+ );
const currentFiles = ref([]);
const toValidateVisas = ref([]);
const createdVisas = ref([]);
@@ -337,20 +356,53 @@ 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 ? files.filter((file) => !matchName(file.name, 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));
+ proceedUpload({ files: [...validFiles, ...finalFiles], folders }, folder);
+ },
+ },
+ });
+ return;
+ }
+
+ proceedUpload({ files, folders }, folder);
+ };
const loadingFileIds = ref([]);
const isCreatingModels = ref(false);
@@ -544,6 +596,50 @@ 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 };
+ }),
+ );
+ openModal({
+ component: NamingConflictModal,
+ props: {
+ project: props.project,
+ documents,
+ allFolders: allFolders.value,
+ rule: null,
+ showRulesByGroup: true,
+ onClose: closeModal,
+ onConfirm: async () => {
+ await props.refreshFiles();
+ },
+ },
+ });
+ };
+
const visasLoading = ref(false);
const openVisaManager = (file) => {
onTabChange(filesTabs[2]);
@@ -683,6 +779,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",
@@ -758,11 +867,15 @@ export default {
watch(
() => props.fileStructure,
(struct) => {
- const folderId = gedTargetFolder.get();
+ const currentFolderId = currentFolder.value?.id;
+
+ if (currentFolderId) {
+ const folder = handler.get({
+ nature: FILE_TYPE.FOLDER,
+ id: currentFolderId,
+ });
- if (folderId) {
- jumpToTargetFolder(folderId);
- gedTargetFolder.clear();
+ currentFolder.value = folder ? handler.deserialize(folder) : struct;
} else {
currentFolder.value = struct;
}
@@ -772,11 +885,15 @@ export default {
watch(
() => currentFolder.value,
- (folder) => {
+ async (folder) => {
+ if (!folder) return;
+
const childrenFolders = folder.children.filter(isFolder).sort(sortByName);
const childrenFiles = folder.children.filter((c) => !isFolder(c)).sort(sortByName);
+
currentFiles.value = childrenFolders.concat(childrenFiles);
gedTargetFolder.set(folder.id);
+ currentFolderNamingRule.value = await getEffectiveFolderRule(props.project, folder);
},
{ immediate: true },
);
@@ -788,6 +905,7 @@ export default {
allTags,
allVisas,
currentFolder,
+ folderWithNamingPreview,
currentSpace,
currentVisa,
displayedFiles,
@@ -829,10 +947,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..290060929 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: {
@@ -148,6 +171,10 @@ export default {
type: Object,
required: true,
},
+ refreshFiles: {
+ type: Function,
+ default: null,
+ },
selectedFileTab: {
type: Object,
required: true,
@@ -157,25 +184,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 +226,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 +269,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 +278,29 @@ 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,
+ refreshFiles: props.refreshFiles,
+ },
+ });
+ };
+
return {
// References
dropdown,
@@ -255,11 +315,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..700d420a1 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 @@
-