diff --git a/package-lock.json b/package-lock.json
index dda3a531b..7641877fc 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.7",
"@paddle/paddle-js": "^1.6.5",
"async": "^3.2.6",
@@ -74,9 +74,9 @@
}
},
"node_modules/@actions/http-client/node_modules/undici": {
- "version": "6.29.1",
- "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.1.tgz",
- "integrity": "sha512-EvRlUt/QqAEvXdQeRMYUfwOQsfzpDztykrG2owGEE/rBOBoYv9uF39ZzsNDh4lTFHhTr2XOZjKkYgueD12ZmGA==",
+ "version": "6.28.1",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.1.tgz",
+ "integrity": "sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -109,9 +109,9 @@
}
},
"node_modules/@babel/parser": {
- "version": "8.0.7",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.7.tgz",
- "integrity": "sha512-UkDwEbPO1px2JQSrdh4qIgaH1JgG7GO/pFeJkpxhDxIETkT/QvGpnigXIw75ViHYPtIgKITmVUJqLjiO+JuWjA==",
+ "version": "8.0.6",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.6.tgz",
+ "integrity": "sha512-LpGDIYJAzc3Y3PT9x+FxBrGYu2PlWxLmyN7SwPTzaX0LSwEmFJBsRnPBqmaXF2r1v+Zhz6lNiEJ+7yKtM5+Ugg==",
"license": "MIT",
"dependencies": {
"@babel/types": "^8.0.6"
@@ -167,9 +167,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.7",
@@ -453,14 +453,14 @@
}
},
"node_modules/@intlify/core-base": {
- "version": "11.4.16",
- "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.16.tgz",
- "integrity": "sha512-xB8nMdMRLNx7b0zPvrHy0+N9uq9Vfi1aUxHqWgn2e2/TbjCKy8HfoFpnUXXxVP8I0p1ccd6LFSC3XsFjZ3vweA==",
+ "version": "11.4.17",
+ "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.17.tgz",
+ "integrity": "sha512-HwKRw4C0n/Nb+d6BcpT6sJdc/XPZEQjY9eMX1+1tvAqDcz0G7ovLQadKnfYvOkk1NH4sskho3rrVLyplnBoP7Q==",
"license": "MIT",
"dependencies": {
- "@intlify/devtools-types": "11.4.16",
- "@intlify/message-compiler": "11.4.16",
- "@intlify/shared": "11.4.16"
+ "@intlify/devtools-types": "11.4.17",
+ "@intlify/message-compiler": "11.4.17",
+ "@intlify/shared": "11.4.17"
},
"engines": {
"node": ">= 22"
@@ -470,13 +470,13 @@
}
},
"node_modules/@intlify/devtools-types": {
- "version": "11.4.16",
- "resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.16.tgz",
- "integrity": "sha512-Bl7arICsKgwcSEQuZJun3swenN4jwSpg35lCpijFznYbW9GvkfH4wLxcI5+gz7d/EHqu66UT9CZDIdnQdgMAGg==",
+ "version": "11.4.17",
+ "resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.17.tgz",
+ "integrity": "sha512-PvB/BdftBBgEfY2PJvK4n61ENr91TWaNQAy46fw8/eLSgQrkUIPJ1gNrkbO0hyE5fY1l1lrWFKab93lNiNVATg==",
"license": "MIT",
"dependencies": {
- "@intlify/core-base": "11.4.16",
- "@intlify/shared": "11.4.16"
+ "@intlify/core-base": "11.4.17",
+ "@intlify/shared": "11.4.17"
},
"engines": {
"node": ">= 22"
@@ -486,12 +486,12 @@
}
},
"node_modules/@intlify/message-compiler": {
- "version": "11.4.16",
- "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.16.tgz",
- "integrity": "sha512-p4lDWmBAsi3ja+PuEfPRavYyvqkYn/Upxz7UpnyI9LgIIy8XawcGIDzN+k99Z+99etO3HmDXGeTxnJIJGgSZ+g==",
+ "version": "11.4.17",
+ "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.17.tgz",
+ "integrity": "sha512-DFFSqirBN54QctjiZibXuS1X5LFtkT/4TNzBKG4lc2sMvPI/LflPWin1u6Sgbz/bB8rlcdcXTbiddNJV64rJYg==",
"license": "MIT",
"dependencies": {
- "@intlify/shared": "11.4.16",
+ "@intlify/shared": "11.4.17",
"source-map-js": "^1.0.2"
},
"engines": {
@@ -502,9 +502,9 @@
}
},
"node_modules/@intlify/shared": {
- "version": "11.4.16",
- "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.16.tgz",
- "integrity": "sha512-xehmkbEgbbNh/V+d8r3hFup0ViaN29YTJSAfRr28C54rxjyZEO0RNQKqW7VVajj16gLdglotNjiq+sIVNtHGzw==",
+ "version": "11.4.17",
+ "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.17.tgz",
+ "integrity": "sha512-oGrg/AL/spD9/kjpR9bch+yd9V4gYxod1txoLQalPs2C+riR1c2bh9T5Wjcb7obh6fxUwrHgnVAp0+FEbC4RfQ==",
"license": "MIT",
"engines": {
"node": ">= 22"
@@ -2092,9 +2092,9 @@
}
},
"node_modules/@vue/compiler-core/node_modules/@babel/helper-string-parser": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
- "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "version": "7.29.11",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz",
+ "integrity": "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -2110,12 +2110,12 @@
}
},
"node_modules/@vue/compiler-core/node_modules/@babel/parser": {
- "version": "7.29.9",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz",
- "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==",
+ "version": "7.29.12",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.12.tgz",
+ "integrity": "sha512-WJ7JBp/Q4+z/ImydtI7jS6NVco5++632ExBemSX+Ysb+G1lCwQ5BVo3SY+jGGOcZXI4ZccbZSUSJLNBW6eEpJg==",
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.29.8"
+ "@babel/types": "^7.29.11"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -2125,12 +2125,12 @@
}
},
"node_modules/@vue/compiler-core/node_modules/@babel/types": {
- "version": "7.29.8",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
- "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
+ "version": "7.29.11",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz",
+ "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==",
"license": "MIT",
"dependencies": {
- "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-string-parser": "^7.29.11",
"@babel/helper-validator-identifier": "^7.29.7"
},
"engines": {
@@ -2165,9 +2165,9 @@
}
},
"node_modules/@vue/compiler-sfc/node_modules/@babel/helper-string-parser": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
- "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "version": "7.29.11",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz",
+ "integrity": "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -2183,12 +2183,12 @@
}
},
"node_modules/@vue/compiler-sfc/node_modules/@babel/parser": {
- "version": "7.29.9",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz",
- "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==",
+ "version": "7.29.12",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.12.tgz",
+ "integrity": "sha512-WJ7JBp/Q4+z/ImydtI7jS6NVco5++632ExBemSX+Ysb+G1lCwQ5BVo3SY+jGGOcZXI4ZccbZSUSJLNBW6eEpJg==",
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.29.8"
+ "@babel/types": "^7.29.11"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -2198,12 +2198,12 @@
}
},
"node_modules/@vue/compiler-sfc/node_modules/@babel/types": {
- "version": "7.29.8",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
- "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
+ "version": "7.29.11",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz",
+ "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==",
"license": "MIT",
"dependencies": {
- "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-string-parser": "^7.29.11",
"@babel/helper-validator-identifier": "^7.29.7"
},
"engines": {
@@ -3190,9 +3190,9 @@
}
},
"node_modules/confbox": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.3.1.tgz",
- "integrity": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==",
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.3.2.tgz",
+ "integrity": "sha512-ZUAKhKlkCOrIGKc8cj7zek9s9z1mveHif/25nUshI2BzCRzF7KH7KHa+Lnh2ubq+e1tIlaFuP763mzW4qVoehg==",
"license": "MIT"
},
"node_modules/config-chain": {
@@ -3439,9 +3439,9 @@
"license": "MIT"
},
"node_modules/cypress": {
- "version": "16.2.0",
- "resolved": "https://registry.npmjs.org/cypress/-/cypress-16.2.0.tgz",
- "integrity": "sha512-qWW9RTldmGDmrWHL6UnsFxuOB3bJ3Kh9Ix7nb5mggI/d2IMPbl7ROd6+iy+zLIBp7ujQOQ8a5JQ6eODUJYsXVQ==",
+ "version": "16.2.1",
+ "resolved": "https://registry.npmjs.org/cypress/-/cypress-16.2.1.tgz",
+ "integrity": "sha512-3Vqf1Krr92hKww/Owob9JrDO7TgBCR2ct+OQrvBOFGSnoGsikL4YF5arqDrJb+SM8Fzi7xXIeVmCeaOqJKPhRA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -4055,9 +4055,9 @@
}
},
"node_modules/eslint": {
- "version": "10.13.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.13.0.tgz",
- "integrity": "sha512-UohDjD4zSzEwhf58M3O7vc/ycc8O1I7dNlhtD0WxE8Px8EHNJKtUwzBqrzeGhisRuv2ag4sAYswszCJRgKXYUg==",
+ "version": "10.14.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.14.0.tgz",
+ "integrity": "sha512-T0pNsxkb9IKmtQjhpK3d7nWOpFv+YHuMbTJnSVMoQblF0onvXfErrtp4V1jHtsQPan7dKaGVD0q1WCXnzMF31A==",
"dev": true,
"license": "MIT",
"workspaces": [
@@ -4070,7 +4070,7 @@
"@eslint/config-helpers": "^0.7.0",
"@eslint/core": "^1.2.1",
"@eslint/plugin-kit": "^0.7.3",
- "@humanfs/node": "^0.16.6",
+ "@humanfs/node": "^0.16.8",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2",
"@types/estree": "^1.0.6",
@@ -4091,7 +4091,7 @@
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
- "minimatch": "^10.2.5",
+ "minimatch": "^10.2.6",
"natural-compare": "^1.4.0",
"optionator": "^0.9.3"
},
@@ -8469,20 +8469,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/obug": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz",
- "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==",
- "dev": true,
- "funding": [
- "https://github.com/sponsors/sxzz",
- "https://opencollective.com/debug"
- ],
- "license": "MIT",
- "engines": {
- "node": ">=12.20.0"
- }
- },
"node_modules/oidc-client-ts": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.5.0.tgz",
@@ -8942,9 +8928,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.31",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz",
- "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==",
+ "version": "8.5.32",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz",
+ "integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==",
"funding": [
{
"type": "opencollective",
@@ -9154,9 +9140,9 @@
"license": "MIT"
},
"node_modules/qs": {
- "version": "6.17.1",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.17.1.tgz",
- "integrity": "sha512-jd3COkaKVgGUW+ZHyEHCG9bH7RoaaiWuMgzsIBjUMVjLAaWGWpoUAVwljVvBHxRu/2qEHnadLGZrkutYUBPQJA==",
+ "version": "6.16.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",
+ "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -9565,9 +9551,9 @@
"license": "MIT"
},
"node_modules/sass": {
- "version": "1.105.2",
- "resolved": "https://registry.npmjs.org/sass/-/sass-1.105.2.tgz",
- "integrity": "sha512-KxW6ZKZiBKWK3ziQ4YZ3cpqo8/1TlHXE8t9nBNvGDZFoBFbk2MD7RacxYFs8zy8hzeFwpFYkVyAhboi47uYPEQ==",
+ "version": "1.107.1",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.107.1.tgz",
+ "integrity": "sha512-aJ1/LvxOOb04YhmiBjzZn3jVwdHKol+OVhoMV6fFl02fF9T3i+5Mg9l/CnzqtM+NX5Jn5o3lAbs/ZJivTCLHfQ==",
"license": "MIT",
"optional": true,
"dependencies": {
@@ -9586,9 +9572,9 @@
}
},
"node_modules/sass-embedded": {
- "version": "1.105.2",
- "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.105.2.tgz",
- "integrity": "sha512-I8uIL7+eE3hPL/Ms2yDY7Km0lsn8xbgqUc231WaWuv+uUphy5LRgRQvMm/MQb9nx/okN9lY47x7sJbVk4o4JYQ==",
+ "version": "1.107.1",
+ "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.107.1.tgz",
+ "integrity": "sha512-DpU0lgDxNE2H7iycTfvvmig1NZ92SqO63D1PPRI8sBniHdpEOzxQno71YRXvEDQgPkaJ7mnljwX16s80AqydaQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -9607,30 +9593,30 @@
"node": ">=20.19.0"
},
"optionalDependencies": {
- "sass-embedded-all-unknown": "1.105.2",
- "sass-embedded-android-arm": "1.105.2",
- "sass-embedded-android-arm64": "1.105.2",
- "sass-embedded-android-riscv64": "1.105.2",
- "sass-embedded-android-x64": "1.105.2",
- "sass-embedded-darwin-arm64": "1.105.2",
- "sass-embedded-darwin-x64": "1.105.2",
- "sass-embedded-linux-arm": "1.105.2",
- "sass-embedded-linux-arm64": "1.105.2",
- "sass-embedded-linux-musl-arm": "1.105.2",
- "sass-embedded-linux-musl-arm64": "1.105.2",
- "sass-embedded-linux-musl-riscv64": "1.105.2",
- "sass-embedded-linux-musl-x64": "1.105.2",
- "sass-embedded-linux-riscv64": "1.105.2",
- "sass-embedded-linux-x64": "1.105.2",
- "sass-embedded-unknown-all": "1.105.2",
- "sass-embedded-win32-arm64": "1.105.2",
- "sass-embedded-win32-x64": "1.105.2"
+ "sass-embedded-all-unknown": "1.107.1",
+ "sass-embedded-android-arm": "1.107.1",
+ "sass-embedded-android-arm64": "1.107.1",
+ "sass-embedded-android-riscv64": "1.107.1",
+ "sass-embedded-android-x64": "1.107.1",
+ "sass-embedded-darwin-arm64": "1.107.1",
+ "sass-embedded-darwin-x64": "1.107.1",
+ "sass-embedded-linux-arm": "1.107.1",
+ "sass-embedded-linux-arm64": "1.107.1",
+ "sass-embedded-linux-musl-arm": "1.107.1",
+ "sass-embedded-linux-musl-arm64": "1.107.1",
+ "sass-embedded-linux-musl-riscv64": "1.107.1",
+ "sass-embedded-linux-musl-x64": "1.107.1",
+ "sass-embedded-linux-riscv64": "1.107.1",
+ "sass-embedded-linux-x64": "1.107.1",
+ "sass-embedded-unknown-all": "1.107.1",
+ "sass-embedded-win32-arm64": "1.107.1",
+ "sass-embedded-win32-x64": "1.107.1"
}
},
"node_modules/sass-embedded-all-unknown": {
- "version": "1.105.2",
- "resolved": "https://registry.npmjs.org/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.105.2.tgz",
- "integrity": "sha512-GqjZMao18JekkkSWIoKOsmxJRGPF1c7fPXK8bn1AP0nrtYzCX7pvid0rmHcCi1Ww6DfDVbjkif52PCCupuPIOQ==",
+ "version": "1.107.1",
+ "resolved": "https://registry.npmjs.org/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.107.1.tgz",
+ "integrity": "sha512-8JVe87KQHSu/uMmcbcRa33AVDdwf0FSCR1tqA9fTaLrsqpgApD2YgqFr7KKCYTNRVSwzqAKdDaD2YJgx/siuVQ==",
"cpu": [
"!arm",
"!arm64",
@@ -9640,13 +9626,13 @@
"license": "MIT",
"optional": true,
"dependencies": {
- "sass": "1.105.2"
+ "sass": "1.107.1"
}
},
"node_modules/sass-embedded-android-arm": {
- "version": "1.105.2",
- "resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.105.2.tgz",
- "integrity": "sha512-BCFgxtW2HvWjedNTRA6dVo8EXxeQzk4dUymfr9A6SO4ReZKS6XkQrFsTEEdmYa7KTtZg9iOunCS6asSk4cmHNw==",
+ "version": "1.107.1",
+ "resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.107.1.tgz",
+ "integrity": "sha512-qst7MmgqFwAHjbDN9Z+zjwMqmPM6RGu5ynXJFZmEmkkjCoQQX8DHUZhNCc2bASeLk/wyz1/9RCziUcJ6kn+E5w==",
"cpu": [
"arm"
],
@@ -9660,9 +9646,9 @@
}
},
"node_modules/sass-embedded-android-arm64": {
- "version": "1.105.2",
- "resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.105.2.tgz",
- "integrity": "sha512-9v5TtqYzpjw0cEqojXPzHrOGqgEOLNbmV8iPEL9RU+dDBoCW6fKfZqYLtbwaB/1fcZMGTZYxiLd8wDLi66IlIw==",
+ "version": "1.107.1",
+ "resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.107.1.tgz",
+ "integrity": "sha512-tk1uq03Jqba2OuktUuIVQ0eSERB4oCcYiZ88wdzX1edWT/Yc7c4x6ECl2+7zqXec6SGIUDZlI86nurN5fTJLHA==",
"cpu": [
"arm64"
],
@@ -9676,9 +9662,9 @@
}
},
"node_modules/sass-embedded-android-riscv64": {
- "version": "1.105.2",
- "resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.105.2.tgz",
- "integrity": "sha512-+1dNjHu0N8GlLTawCHU8zrqLQms52aof6WYoExGp1BO3xHH7eCnxyIxd2FWOj2ttDhUyEpK8Odp+gYaxhpNMIw==",
+ "version": "1.107.1",
+ "resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.107.1.tgz",
+ "integrity": "sha512-WzBSIg44+XP15kc+G2qtpfYBm0BtxjuC/X11eEg2L5Ihw45DoBtSLkpkSpLd9tvxDJOiTK3AHd1/iv5oGNvibg==",
"cpu": [
"riscv64"
],
@@ -9692,9 +9678,9 @@
}
},
"node_modules/sass-embedded-android-x64": {
- "version": "1.105.2",
- "resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.105.2.tgz",
- "integrity": "sha512-+4Ga77s76LSCml6n2kJ8tDtRuwS/T83OCEvsGp0BCz8HX3L9edFGmHO3ryYcQEbmxiNlhc3tzuVW/enfhiefqw==",
+ "version": "1.107.1",
+ "resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.107.1.tgz",
+ "integrity": "sha512-K6YN7kqj9aBLmraVDiPIHF7U8qoD9E/6AJIVwj1Y5H0EgPjGOsv1sYu9Uy6SBLvWGg4dsydhbPU7VC+hTGWk9A==",
"cpu": [
"x64"
],
@@ -9708,9 +9694,9 @@
}
},
"node_modules/sass-embedded-darwin-arm64": {
- "version": "1.105.2",
- "resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.105.2.tgz",
- "integrity": "sha512-aVRTgD8yisk6q3SWnI0CquIU5OiWtCBBmZHEoQMQdlqMBJLmO8+gSQ5qNfBkn4shkiY93dEDpK7xP3vsQzicXA==",
+ "version": "1.107.1",
+ "resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.107.1.tgz",
+ "integrity": "sha512-okhyzPI4cLNwa/xRIWbpkRGKmebaNp1+15V5PxVsQbzd8c+dn7eZje2Vf5CWxv9jRZ0nJgM1K/F61Wwb0w8Tyg==",
"cpu": [
"arm64"
],
@@ -9724,9 +9710,9 @@
}
},
"node_modules/sass-embedded-darwin-x64": {
- "version": "1.105.2",
- "resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.105.2.tgz",
- "integrity": "sha512-Bsfijwi4VkB2Uva6LEbA9AGeGd56HmnN36hjsfa9ibsQdLy3ZPwjFMujQCkYrrA4ISxxymMX3vMmDO8RB/F0qg==",
+ "version": "1.107.1",
+ "resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.107.1.tgz",
+ "integrity": "sha512-hMMcxMxTiWoMaCfEvZqofN2l/yW++S0ANdVA3uHbVYO1LeRf9MrOWlOkJCjsbJO5FvfZdKGUxq/hc/5FKX9PXQ==",
"cpu": [
"x64"
],
@@ -9740,9 +9726,9 @@
}
},
"node_modules/sass-embedded-linux-arm": {
- "version": "1.105.2",
- "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.105.2.tgz",
- "integrity": "sha512-JJ4UYZweLZTU2f5lbjJ8Z/L018X8Y0ennRabFeVVRws43F5TXu3tKevwi4mg7kM97kI7jArfyIZIkyoHlU1opQ==",
+ "version": "1.107.1",
+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.107.1.tgz",
+ "integrity": "sha512-k/UIsseDf63xo5qwV1kZgTmJtFlBAKLQf3xb++NmQPv7zvdOZxJ/MVUd1cuDx7vuanVKhM+U6R1mW2GuQxEGHA==",
"cpu": [
"arm"
],
@@ -9757,9 +9743,9 @@
}
},
"node_modules/sass-embedded-linux-arm64": {
- "version": "1.105.2",
- "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.105.2.tgz",
- "integrity": "sha512-brEilUD9qlnzau/FiYc2k6h1+6WpByktuj/mAPEFftvLEXtes+e3/TTdTMUlS2RFkdeM0FDhIwjrkMSpYqEmow==",
+ "version": "1.107.1",
+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.107.1.tgz",
+ "integrity": "sha512-cDeuJi63cL2cfr9OZoB2z1jd7Omhdgbj7zBxawZ62NFLe/gYVgeUsHbkB0rh1xbsuMJ9nOu9Mu6bkw8N0XnigQ==",
"cpu": [
"arm64"
],
@@ -9774,9 +9760,9 @@
}
},
"node_modules/sass-embedded-linux-musl-arm": {
- "version": "1.105.2",
- "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.105.2.tgz",
- "integrity": "sha512-s2mPkPYtW97l+01VxAeivGBAU/nRdnppb5m5ZfydAwn5lU9dOua8yy+fXKmoSNn4m9yxn0nZief0miY75JyoLg==",
+ "version": "1.107.1",
+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.107.1.tgz",
+ "integrity": "sha512-jshFSmPp7/tq3zV5l7KEDWuZKN6aewTnKMkWzgXN4oiBvRKFWAeQqG2Acn+gjMwtrvY0aRGZxrgreJGwRmFZlg==",
"cpu": [
"arm"
],
@@ -9791,9 +9777,9 @@
}
},
"node_modules/sass-embedded-linux-musl-arm64": {
- "version": "1.105.2",
- "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.105.2.tgz",
- "integrity": "sha512-wPj0bgvkDrKZQV4uFXMJu0v61LdBaWpcZU/ok2xce4Rdu3PC0LP+2DeLBssrDU6A0GFxxkQwVdb4xcSCWVJt4A==",
+ "version": "1.107.1",
+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.107.1.tgz",
+ "integrity": "sha512-Pr7kApBThIJjKG7FkGIsFvYrhj8aCu7kEuov7WiK3ExUiv5m+Vz8eYzjqIEGGCAyVIGMuuvHCvPmuMUynPZ1CA==",
"cpu": [
"arm64"
],
@@ -9808,9 +9794,9 @@
}
},
"node_modules/sass-embedded-linux-musl-riscv64": {
- "version": "1.105.2",
- "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.105.2.tgz",
- "integrity": "sha512-ut/LWn4yV1718FopQxU4pbeeyxNUOsYeoUjqtmTa0AI4j+Ruy9V+yCMNVfZXxrbYmlQ/6KUDZPcqZPJqKG5M8g==",
+ "version": "1.107.1",
+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.107.1.tgz",
+ "integrity": "sha512-5oJVR7xt0yQnIlW/1Xz+H5Bl2Tix5Gl4qrlrVvUCzfdFUWBwLSZi6KlabjC41UjRisandO3nxHXPZeI1T99krQ==",
"cpu": [
"riscv64"
],
@@ -9825,9 +9811,9 @@
}
},
"node_modules/sass-embedded-linux-musl-x64": {
- "version": "1.105.2",
- "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.105.2.tgz",
- "integrity": "sha512-h+IMF36ymyshJCahILllQPOFh4Z9V6xDu2sE9MY9eJkYPT8KlJ9baQh0l7mnkSMMch7H4bIjd8srEW7mYQKilw==",
+ "version": "1.107.1",
+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.107.1.tgz",
+ "integrity": "sha512-ntHvwls0ImF6p9qgZBuIF+BKIZ+6FqrRKZn0JhiFzb48y5o/V0mre6YEmdq8YcYVxPrGaU+Ajk96lpTE7gUC8w==",
"cpu": [
"x64"
],
@@ -9842,9 +9828,9 @@
}
},
"node_modules/sass-embedded-linux-riscv64": {
- "version": "1.105.2",
- "resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.105.2.tgz",
- "integrity": "sha512-huWVMN6VGxo8Prc7c5K8rYxmENNuMcD4Q2Jm/4LtoL0+HU8OeCq/ixRSXd/D+pGNiwzh6Kq6S3rX3PIfAVwd/g==",
+ "version": "1.107.1",
+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.107.1.tgz",
+ "integrity": "sha512-m8zLEdUPafxCAXtqx5FgBylIlPaUcb4eBx1lE8ENp/kaqqTMmaapofl+YWFI8xII3id+4rePZBipR142vHnNpQ==",
"cpu": [
"riscv64"
],
@@ -9859,9 +9845,9 @@
}
},
"node_modules/sass-embedded-linux-x64": {
- "version": "1.105.2",
- "resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.105.2.tgz",
- "integrity": "sha512-ms4LEVWt6+yIbRAOosPMoHpQPT+60+x9KJM6JSviTvBsOhtjbT/Y+otdpAoXotjE3VDgOshnOAUxyazluUIvcg==",
+ "version": "1.107.1",
+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.107.1.tgz",
+ "integrity": "sha512-ix29w9TrUrvpzjOir+Ndx3RH5VLJncjq6CMystVUVbx5rpYyyaSzciOm5NKF227ifivhuhWYOFPCkq1NedmXfQ==",
"cpu": [
"x64"
],
@@ -9876,9 +9862,9 @@
}
},
"node_modules/sass-embedded-unknown-all": {
- "version": "1.105.2",
- "resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.105.2.tgz",
- "integrity": "sha512-mRCho+NEe4AN4x3QKiRBy31dZwivfsYZFYuqzdN4PpiQfquY/IzwgbMgWp6w1/wDMKr5oUyk000YuhKCiVR2Sg==",
+ "version": "1.107.1",
+ "resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.107.1.tgz",
+ "integrity": "sha512-7JKs9q+V/kyVBesAjMa8hXx/qk6/cGKq/7cPSZoAWGu6oHx41wiJVfkZVAYt+fOFRHFXQunIKxIR9ZV7nA63PA==",
"license": "MIT",
"optional": true,
"os": [
@@ -9888,13 +9874,13 @@
"!win32"
],
"dependencies": {
- "sass": "1.105.2"
+ "sass": "1.107.1"
}
},
"node_modules/sass-embedded-win32-arm64": {
- "version": "1.105.2",
- "resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.105.2.tgz",
- "integrity": "sha512-UtpUt3B7kRyymLSmEcW0qCTU89qH3r5xaboaKD5Fb3x+PzGA/I3rNu0yn1GI+94Qp4BkAqbLXNdd1Dda7rOC/g==",
+ "version": "1.107.1",
+ "resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.107.1.tgz",
+ "integrity": "sha512-/DmYPuToDvhsT4Oz1RQuOvQuYFzhcjzAfxjwhRPdWXxGO1OR8+BhbxyXqOUMpMLwgFT9Du3GPjD1yvy8BXnTZQ==",
"cpu": [
"arm64"
],
@@ -9908,9 +9894,9 @@
}
},
"node_modules/sass-embedded-win32-x64": {
- "version": "1.105.2",
- "resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.105.2.tgz",
- "integrity": "sha512-O5eP1eUuyaU7RgBgA3/aYKXUT3FqHo11C5MbnHqwBDSMY5tZ+nMqcLVS0yZpzkJs3JOWj3fO6o/6fG+b+PzMpA==",
+ "version": "1.107.1",
+ "resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.107.1.tgz",
+ "integrity": "sha512-B25UYFtuhnYiqvfLNlJZyDp2xV7T0aT9nueE2w+QhDPM/wCZyeQfj1dgMgq0HYBwEybgx0oLuxomgTqplmQ+eg==",
"cpu": [
"x64"
],
@@ -10705,9 +10691,9 @@
}
},
"node_modules/systeminformation": {
- "version": "5.33.19",
- "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.33.19.tgz",
- "integrity": "sha512-Zm7wGeCG7PqfvzegMJIzrxICopWqYCeLATVmvxOOci1isLcNVKgY4Fr0hhYQzvc7cgtK3377jQVChTFAnznOiA==",
+ "version": "5.33.11",
+ "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.33.11.tgz",
+ "integrity": "sha512-CNGk+882EQbsbPzaoxeBcYyx+nk0uTj5JzW8w0EXGSCazndNRAh9SE0V5tyMbdONGKilbIBJsyskF4WmZDk7uA==",
"dev": true,
"license": "MIT",
"os": [
@@ -10860,9 +10846,9 @@
}
},
"node_modules/tinybench": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.2.tgz",
- "integrity": "sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==",
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.3.tgz",
+ "integrity": "sha512-Al01C2JIBnO2CXCAPbpAl6sUiOZHeThGtHYNTa51JRw9PyWOCdr+9Ong05R4qg/Sk3+H645E8ov1h2c58ityNw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -11061,9 +11047,9 @@
}
},
"node_modules/undici": {
- "version": "7.30.1",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.1.tgz",
- "integrity": "sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg==",
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz",
+ "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -11458,6 +11444,20 @@
"@jridgewell/sourcemap-codec": "^1.6.0"
}
},
+ "node_modules/vitest/node_modules/obug": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz",
+ "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
"node_modules/vue": {
"version": "3.5.44",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.44.tgz",
@@ -11505,14 +11505,14 @@
}
},
"node_modules/vue-i18n": {
- "version": "11.4.16",
- "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.16.tgz",
- "integrity": "sha512-Kv9tp6iTmaAeOwop7hkAo28N1IBsbSVpnxyE26b6gqc4Q2E9Ke3ZHHA3oqqwS6/cYLT1henZU4etTaLQxrK98Q==",
+ "version": "11.4.17",
+ "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.17.tgz",
+ "integrity": "sha512-Wm3ovbi5aWQ/k0Wv26y/36/9F6nmaCInS05GZwzbc4ZCZX4mdG0J70qGt7U/RGkakFl6h0rPc6Bo0Licc5Ctsw==",
"license": "MIT",
"dependencies": {
- "@intlify/core-base": "11.4.16",
- "@intlify/devtools-types": "11.4.16",
- "@intlify/shared": "11.4.16",
+ "@intlify/core-base": "11.4.17",
+ "@intlify/devtools-types": "11.4.17",
+ "@intlify/shared": "11.4.17",
"@vue/devtools-api": "^6.5.0"
},
"engines": {
@@ -11583,9 +11583,9 @@
}
},
"node_modules/vue-router/node_modules/magic-string": {
- "version": "1.4.3",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz",
- "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==",
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.5.tgz",
+ "integrity": "sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.6.0"
diff --git a/package.json b/package.json
index 8a51cfe0e..1d65aace9 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.7",
"@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 4c48b836c..c60bc9d3f 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)"
@@ -289,6 +290,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..49f5e0db4 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,54 @@ 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,
+ allFolders: allFolders.value,
+ 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 +597,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 +780,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 +868,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 +886,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 +906,7 @@ export default {
allTags,
allVisas,
currentFolder,
+ folderWithNamingPreview,
currentSpace,
currentVisa,
displayedFiles,
@@ -829,10 +948,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..c60d1abaa 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
@@ -19,22 +19,23 @@
class="file-actions-cell__menu"
v-show="isOpen"
:menuItems="menuItems"
+ width="210px"
>
-
-
-
-
-
- {{ $t(item.text) }}
-
+
+
+
+
+
+ {{ $t(item.text) }}
+
@@ -50,7 +51,7 @@ import {
isConvertibleToPhotosphere,
isModel,
isViewable,
- openInViewer
+ openInViewer,
} from "../../../../../utils/models.js";
import { dropdownPositioner } from "../../../../../utils/positioner.js";
// Components
@@ -60,19 +61,19 @@ import SetAsModelIcon from "../../../../../components/images/SetAsModelIcon.vue"
export default {
props: {
parent: {
- type: Object
+ type: Object,
},
project: {
type: Object,
- required: true
+ required: true,
},
file: {
type: Object,
- required: true
+ required: true,
},
loading: {
type: Boolean,
- required: true
+ required: true,
},
},
emits: [
@@ -82,6 +83,7 @@ export default {
"download",
"file-clicked",
"manage-access",
+ "manage-naming-rule",
"open-tag-manager",
"open-versioning-manager",
"open-visa-manager",
@@ -96,6 +98,8 @@ export default {
const isOpen = ref(false);
const menuItems = shallowRef([]);
+ let current_key = 0;
+
const openMenu = () => {
if (!props.parent) return;
@@ -103,7 +107,7 @@ export default {
if (!isFolder(props.file)) {
menuItems.value.push({
- key: 1,
+ key: current_key++,
icon: "preview",
text: "FileActionsCell.previewModelButtonText",
color: "primary",
@@ -114,7 +118,7 @@ export default {
if (isViewable(props.file)) {
const { model_id: id, model_type: type } = props.file;
menuItems.value.push({
- key: 2,
+ key: current_key++,
icon: "show",
text: "FileActionsCell.openViewerButtonText",
color: "primary",
@@ -125,15 +129,15 @@ export default {
if (!isFolder(props.file) && isConvertible(props.file)) {
if (!isModel(props.file)) {
menuItems.value.push({
- key: 3,
+ key: current_key++,
iconComponent: SetAsModelIcon,
text: "FileActionsCell.createModelButtonText",
disabled: !hasAdminPerm(props.project, props.file),
- action: () => onClick("create-model")
+ action: () => onClick("create-model"),
});
} else {
menuItems.value.push({
- key: 4,
+ key: current_key++,
iconComponent: RemoveModelsIcon,
text: "FileActionsCell.removeModelButtonText",
action: () => onClick("remove-model"),
@@ -143,16 +147,16 @@ export default {
if (!isFolder(props.file) && isConvertibleToPhotosphere(props.file) && !isModel(props.file)) {
menuItems.value.push({
- key: 3,
+ key: current_key++,
iconComponent: SetAsModelIcon,
text: "FileActionsCell.createPhotosphereButtonText",
disabled: !hasAdminPerm(props.project, props.file),
- action: () => onClick("create-photosphere")
+ action: () => onClick("create-photosphere"),
});
}
menuItems.value.push({
- key: 5,
+ key: current_key++,
icon: "edit",
text: "t.rename",
disabled: !hasAdminPerm(props.project, props.file),
@@ -160,7 +164,7 @@ export default {
});
menuItems.value.push({
- key: 6,
+ key: current_key++,
icon: "download",
text: "t.download",
action: () => onClick("download"),
@@ -168,7 +172,14 @@ export default {
if (isFolder(props.file) && isProjectAdmin(props.project)) {
menuItems.value.push({
- key: 7,
+ key: current_key++,
+ iconComponent: "BIMDataIconNamingConvention",
+ text: "NamingConstraint.folderRuleMenuItem",
+ action: () => onClick("manage-naming-rule"),
+ dataTestId: "btn-manage-naming-rule",
+ });
+ menuItems.value.push({
+ key: current_key++,
icon: "key",
text: "FileActionsCell.manageAccessButtonText",
action: () => onClick("manage-access"),
@@ -178,21 +189,21 @@ export default {
if (!isFolder(props.file) && hasAdminPerm(props.project, props.file)) {
menuItems.value.push({
- key: 8,
+ key: current_key++,
icon: "visa",
text: "FileActionsCell.visaButtonText",
action: () => onClick("open-visa-manager"),
dataTestId: "btn-open-visa-manager",
});
menuItems.value.push({
- key: 9,
+ key: current_key++,
icon: "tag",
text: "FileActionsCell.addTagsButtonText",
action: () => onClick("open-tag-manager"),
dataTestId: "btn-open-tag-manager",
});
menuItems.value.push({
- key: 10,
+ key: current_key++,
icon: "versioning",
text: "FileActionsCell.versioningButtonText",
action: () => onClick("open-versioning-manager"),
@@ -202,7 +213,7 @@ export default {
}
menuItems.value.push({
- key: 11,
+ key: current_key++,
icon: "delete",
text: "t.delete",
color: "high",
@@ -214,10 +225,7 @@ export default {
nextTick(() => {
if (props.parent) {
- menu.value.$el.style.top = dropdownPositioner(
- props.parent.$el,
- menu.value.$el
- );
+ menu.value.$el.style.top = dropdownPositioner(props.parent.$el, menu.value.$el);
}
});
};
@@ -230,7 +238,7 @@ export default {
});
};
- const onClick = event => {
+ const onClick = (event) => {
closeMenu();
emit(event);
};
@@ -242,9 +250,9 @@ export default {
menuItems,
// Methods
closeMenu,
- openMenu
+ openMenu,
};
- }
+ },
};
diff --git a/src/components/specific/files/files-table/file-name-cell/FileNameCell.scss b/src/components/specific/files/files-table/file-name-cell/FileNameCell.scss
index 76411a201..4f058fe39 100644
--- a/src/components/specific/files/files-table/file-name-cell/FileNameCell.scss
+++ b/src/components/specific/files/files-table/file-name-cell/FileNameCell.scss
@@ -6,9 +6,12 @@
}
&__update-form {
- display: flex;
- align-items: center;
- gap: var(--spacing-unit);
+ padding: calc(var(--spacing-unit) / 2) 0;
+ gap: calc(var(--spacing-unit) / 2);
+
+ & > .flex {
+ gap: calc(var(--spacing-unit) / 2);
+ }
&__input {
flex-grow: 1;
@@ -23,6 +26,9 @@
display: flex;
align-items: center;
gap: calc(var(--spacing-unit) / 2);
- cursor: pointer;
+ .bimdata-textbox,
+ .bimdata-tooltip {
+ cursor: pointer;
+ }
}
}
diff --git a/src/components/specific/files/files-table/file-name-cell/FileNameCell.vue b/src/components/specific/files/files-table/file-name-cell/FileNameCell.vue
index d0e266ef9..552ade01d 100644
--- a/src/components/specific/files/files-table/file-name-cell/FileNameCell.vue
+++ b/src/components/specific/files/files-table/file-name-cell/FileNameCell.vue
@@ -5,39 +5,59 @@
-
-
-
+
+
+
+
+
import { ref, watch, computed, onBeforeUnmount } from "vue";
+import { useI18n } from "vue-i18n";
import { useFiles } from "../../../../../state/files.js";
+import { useNamingConstraints } from "../../../../../state/naming-constraints.js";
+import { useAppNotification } from "../../../app/app-notification/app-notification.js";
import { debounce } from "../../../../../utils/async.js";
+import { collectDescendants } from "../../../../../utils/file-tree.js";
import { isFolder } from "../../../../../utils/file-structure.js";
+import { matchName, buildExample } from "../../../../../utils/naming-constraint.js";
import { isConvertible, isConvertibleToPhotosphere } from "../../../../../utils/models.js";
+import { useAppModal } from "../../../app/app-modal/app-modal.js";
+
+import NamingConstraintPreview from "../../naming-constraint/naming-constraint-preview/NamingConstraintPreview.vue";
+import NamingConflictModal from "../../naming-constraint/NamingConflictModal.vue";
export default {
+ components: {
+ NamingConstraintPreview,
+ },
props: {
project: {
type: Object,
@@ -85,6 +118,10 @@ export default {
type: Object,
required: true,
},
+ namingConstraintPreview: {
+ type: Object,
+ default: null,
+ },
editMode: {
type: Boolean,
default: false,
@@ -92,18 +129,49 @@ export default {
},
emits: ["close", "file-clicked", "open-versioning-manager", "success"],
setup(props, { emit }) {
- const { updateFiles } = useFiles();
+ const { t } = useI18n();
+ const { updateFiles, projectFileStructure } = useFiles();
+ const { getEffectiveFolderRule } = useNamingConstraints();
+ const { pushNotification } = useAppNotification();
+ const { openModal, closeModal } = useAppModal();
const loading = ref(false);
const nameInput = ref(null);
const fileName = ref("");
const hasError = ref(false);
+ const errorMessage = ref("");
const hasHistory = computed(() => props.file?.history_count > 0);
+ const allFolders = computed(() =>
+ projectFileStructure.value ? collectDescendants(projectFileStructure.value, isFolder) : [],
+ );
+
const renameFile = debounce(async () => {
if (fileName.value) {
+ const rule = isFolder(props.file)
+ ? null
+ : await getEffectiveFolderRule(props.project, {
+ id: props.file.parent_id,
+ });
+ if (rule && !matchName(fileName.value, rule)) {
+ if (rule.strict) {
+ hasError.value = true;
+ errorMessage.value = t("t.invalidNameFormat", {
+ example: buildExample(rule),
+ });
+ nameInput.value.focus();
+ return;
+ }
+ pushNotification({
+ type: "warning",
+ title: t("NamingConstraint.applyRuleWarningTitle"),
+ message: t("t.invalidNameFormat", {
+ example: buildExample(rule),
+ }),
+ });
+ }
try {
loading.value = true;
await updateFiles(props.project, [
@@ -119,6 +187,7 @@ export default {
}
} else {
hasError.value = true;
+ errorMessage.value = t("t.invalidName");
nameInput.value.focus();
}
}, 500);
@@ -135,6 +204,7 @@ export default {
clearTimeout(focusTimeout);
loading.value = false;
hasError.value = false;
+ errorMessage.value = "";
showUpdateForm.value = false;
emit("close");
};
@@ -168,10 +238,29 @@ export default {
}
};
+ const openNamingConflictModal = async () => {
+ const rule = await getEffectiveFolderRule(props.project, {
+ id: props.file.parent_id,
+ });
+
+ openModal({
+ component: NamingConflictModal,
+ props: {
+ project: props.project,
+ documents: [props.file],
+ allFolders: allFolders.value,
+ rule,
+ onClose: closeModal,
+ onConfirm: closeModal,
+ },
+ });
+ };
+
return {
// References
fileName,
hasError,
+ errorMessage,
loading,
nameInput,
showUpdateForm,
@@ -182,6 +271,7 @@ export default {
isFolder,
openUpdateForm,
renameFile,
+ openNamingConflictModal,
};
},
};
diff --git a/src/components/specific/files/files-table/file-type-cell/FileTypeCell.scss b/src/components/specific/files/files-table/file-type-cell/FileTypeCell.scss
index 6d7ea54bc..a0e217d44 100644
--- a/src/components/specific/files/files-table/file-type-cell/FileTypeCell.scss
+++ b/src/components/specific/files/files-table/file-type-cell/FileTypeCell.scss
@@ -23,4 +23,19 @@
border: 1px solid var(--color-white);
}
}
+ .info {
+ height: 9px;
+ width: 9px;
+ position: absolute;
+ top: 0;
+ right: 12px;
+ cursor: pointer;
+ .round {
+ width: 9px;
+ height: 9px;
+ border: 1px solid white;
+ background-color: var(--color-warning);
+ border-radius: 50%;
+ }
+ }
}
diff --git a/src/components/specific/files/files-table/file-type-cell/FileTypeCell.vue b/src/components/specific/files/files-table/file-type-cell/FileTypeCell.vue
index 6beab4d7e..8ed8ab454 100644
--- a/src/components/specific/files/files-table/file-type-cell/FileTypeCell.vue
+++ b/src/components/specific/files/files-table/file-type-cell/FileTypeCell.vue
@@ -2,26 +2,52 @@
- 0
- {{ file.children.length }}
+
+ 0
+
+
+ {{ file.children.length }}
+
+
+
+
diff --git a/src/components/specific/files/folder-table/FoldersTable.vue b/src/components/specific/files/folder-table/FoldersTable.vue
index 1ca35e95b..51bad7903 100644
--- a/src/components/specific/files/folder-table/FoldersTable.vue
+++ b/src/components/specific/files/folder-table/FoldersTable.vue
@@ -63,6 +63,7 @@
props.files.map((file) => ({
...file,
type: isFolder(file) ? t("t.folder") : file.name ? formatExtension(file.name) : t("t.file"),
+ hasNamingConflict: isFolder(file)
+ ? props.hasNamingConflict(file)
+ : file.naming_constraint_conflict,
})),
);
diff --git a/src/components/specific/files/naming-constraint/FolderNamingConstraintManager.vue b/src/components/specific/files/naming-constraint/FolderNamingConstraintManager.vue
new file mode 100644
index 000000000..4ba4522b2
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/FolderNamingConstraintManager.vue
@@ -0,0 +1,166 @@
+
+
+
+
+
+
+
diff --git a/src/components/specific/files/naming-constraint/NamingConflictModal.css b/src/components/specific/files/naming-constraint/NamingConflictModal.css
new file mode 100644
index 000000000..926d39447
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/NamingConflictModal.css
@@ -0,0 +1,91 @@
+.naming-conflict-modal {
+ .header {
+ .icon {
+ width: 40px;
+ min-width: 40px;
+ height: 40px;
+ border-radius: 50px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+ strong {
+ margin-bottom: 3px;
+ font-size: 22px;
+ }
+ span {
+ color: var(--color-granite);
+ }
+ }
+}
+
+.naming-conflict-modal--strict {
+ .header {
+ .icon {
+ background-color: var(--color-high-lighter);
+ }
+ }
+}
+
+.naming-conflict-modal--no-strict {
+ .header {
+ .icon {
+ background-color: var(--color-warning-lighter);
+ }
+ }
+}
+
+.naming-conflict-modal__content {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ text-align: left;
+}
+.naming-conflict-modal__rule {
+ gap: 6px;
+ padding: 12px;
+ border-radius: 6px;
+ background: var(--color-primary-lighter);
+ border: 1px solid #dbe4f0;
+}
+.naming-conflict-modal__rule__name {
+ font-weight: 700;
+ font-size: 12px;
+ color: var(--color-primary);
+}
+.naming-conflict-modal__rule__chip {
+ width: fit-content;
+ padding: 3px 6px;
+ background-color: var(--color-neutral-lighter);
+ color: var(--color-neutral);
+ border-radius: 6px;
+ font-size: 12px;
+}
+.naming-conflict-modal__rule__preview {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 3px;
+ margin-top: 8px;
+}
+
+.naming-conflict-modal__rule__separator, .naming-conflict-modal__rule__extension {
+ color: #64748b;
+ font-weight: 700;
+}
+
+.naming-conflict-modal__intro, .naming-conflict-modal__warning {
+ margin: 0;
+ font-size: 14px;
+ color: var(--color-granite);
+}
+.naming-conflict-modal__warning {
+ padding: 12px 14px;
+ font-size: 14px;
+ line-height: 1.45;
+ font-weight: 600;
+ background-color: var(--color-high-lighter);
+ border: 1px solid #fecdd3;
+ color: #991b1b;
+ border-radius: 6px;
+}
\ No newline at end of file
diff --git a/src/components/specific/files/naming-constraint/NamingConflictModal.vue b/src/components/specific/files/naming-constraint/NamingConflictModal.vue
new file mode 100644
index 000000000..2b5fbee0d
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/NamingConflictModal.vue
@@ -0,0 +1,191 @@
+
+
+
+
+
+
+
+
+
+ {{ $t("NamingConstraint.conflictModalWarning") }}
+
+
+
{{ $t("NamingConstraint.modal.expectedConventionLabel") }}
+
+
+
+
+
+
+
+
+ {{ $t("t.cancel") }}
+
+
+ {{ $t("t.confirm") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ $t("NamingConstraint.modal.expectedConventionLabel") }}
+
+
+
+
+
+
+
+
+ {{ $t("t.cancel") }}
+
+
+ {{
+ allValid
+ ? $t("NamingConstraint.applyRenameButton")
+ : $t("NamingConstraint.confirmRenameButton")
+ }}
+
+
+
+
+
+
+
+
+
diff --git a/src/components/specific/files/naming-constraint/NamingConstraintManagerCommon.css b/src/components/specific/files/naming-constraint/NamingConstraintManagerCommon.css
new file mode 100644
index 000000000..f1078b117
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/NamingConstraintManagerCommon.css
@@ -0,0 +1,59 @@
+.naming-constraint-manager {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ background-color: var(--color-white);
+}
+
+.naming-constraint-manager__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ min-height: 34px;
+}
+
+.naming-constraint-manager__header__icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 40px;
+}
+
+/*.naming-constraint-manager__header__back {
+ margin-left: -6px;
+
+} */
+
+.naming-constraint-manager__header__title {
+ flex-grow: 1;
+ font-size: 14px;
+ color: var(--color-primary);
+ text-align: center;
+}
+
+.naming-constraint-manager__content {
+ position: relative;
+ flex: 1;
+ overflow-y: auto;
+}
+
+.naming-constraint-manager__loader {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: rgba(255, 255, 255, 0.6);
+ z-index: 1;
+}
+
+.fade-enter-active,
+.fade-leave-active {
+ transition: opacity 0.15s ease;
+}
+
+.fade-enter-from,
+.fade-leave-to {
+ opacity: 0;
+}
\ No newline at end of file
diff --git a/src/components/specific/files/naming-constraint/NamingConstraintsManager.vue b/src/components/specific/files/naming-constraint/NamingConstraintsManager.vue
new file mode 100644
index 000000000..3f8556970
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/NamingConstraintsManager.vue
@@ -0,0 +1,282 @@
+
+
+
+
+
+
+
+ {{ tab.label }}
+
+
+
+
+
+
+ {{ $t("NamingConstraint.safeZoneModal.title") }}
+
+
+
+ {{ $t("NamingConstraint.safeZoneModal.text") }}
+
+
+
+
+ {{ $t("t.cancel") }}
+
+
+
+ {{ $t("t.ok") }}
+
+
+
+
+
+
+
+
+
diff --git a/src/components/specific/files/naming-constraint/conflicting-document-item/ConflictingDocumentItem.css b/src/components/specific/files/naming-constraint/conflicting-document-item/ConflictingDocumentItem.css
new file mode 100644
index 000000000..9a3e72c1d
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/conflicting-document-item/ConflictingDocumentItem.css
@@ -0,0 +1,92 @@
+.conflicting-document-item {
+ width: 100%;
+}
+
+.conflicting-document-item__summary {
+ padding: 12px;
+ cursor: pointer;
+ list-style: none;
+}
+
+.conflicting-document-item__summary::-webkit-details-marker {
+ display: none;
+}
+
+.summary__row {
+ gap: 12px;
+}
+
+.summary__column {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+}
+
+.summary__label {
+ margin-bottom: 4px;
+ color: var(--color-primary);
+ font-size: 9px;
+ font-weight: 600;
+ text-transform: uppercase;
+}
+
+.summary__value {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.summary__status {
+ font-size: 13px;
+ font-weight: 500;
+}
+
+.summary__status--valid {
+ color: var(--color-success);
+}
+
+.summary__status--invalid {
+ color: var(--color-high);
+}
+
+.summary__actions {
+ gap: 12px;
+ justify-content: flex-end;
+}
+
+.conflicting-document-item__content {
+ padding: 12px;
+ background: var(--color-white);
+ border-radius: 0 0 8px 8px;
+}
+
+.conflicting-document-item__info .file-path {
+ color: var(--color-granite);
+ font-size: 12px;
+}
+
+.conflicting-document-item__info .file-path span {
+ color: var(--color-primary);
+ font-weight: 600;
+}
+
+.conflicting-document-item__example {
+ margin-top: 8px;
+ color: var(--color-granite);
+}
+
+.conflicting-document-item__rename {
+ display: flex;
+ gap: 12px;
+ margin-bottom: 12px;
+}
+
+.conflicting-document-item__rename .bimdata-input {
+ width: 100%;
+}
+
+.conflicting-document-item__actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 12px;
+}
diff --git a/src/components/specific/files/naming-constraint/conflicting-document-item/ConflictingDocumentItem.vue b/src/components/specific/files/naming-constraint/conflicting-document-item/ConflictingDocumentItem.vue
new file mode 100644
index 000000000..3f1edde4b
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/conflicting-document-item/ConflictingDocumentItem.vue
@@ -0,0 +1,146 @@
+
+
+
+
+
+
+ {{ $t("NamingConstraint.conflictCurrentNameLabel") }}
+
+
+
+
+
+
+
+
+
{{ $t("NamingConstraint.conflictNewNameLabel") }}
+
+
+ {{ generatedName }}
+
+
+
+
+ {{ $t("t.status") }}
+
+
+ {{
+ deleted
+ ? $t("NamingConstraint.conflictStatusDeleted")
+ : valid
+ ? $t("NamingConstraint.conflictStatusValid")
+ : $t("NamingConstraint.conflictStatusInvalid")
+ }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t("NamingConstraint.conflictSourceFolderLabel") }}
+
+ {{ folderPath.map((folder) => folder.name).join(" / ") }}
+
+
+ {{ $t("t.rootFolder") }}
+
+
+
+
+
+
+
+
+
+ {{ $t("NamingConstraint.conflictDeleteFileButton") }}
+
+
+
+
+ {{ $t("NamingConstraint.conflictRestoreFileButton") }}
+
+
+ {{ $t("NamingConstraint.renameButton") }}
+
+
+
+
+
+
+
+
+
diff --git a/src/components/specific/files/naming-constraint/conflicting-document-item/naming-constraint-file-editor/NamingConstraintFileEditor.css b/src/components/specific/files/naming-constraint/conflicting-document-item/naming-constraint-file-editor/NamingConstraintFileEditor.css
new file mode 100644
index 000000000..fae063c7d
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/conflicting-document-item/naming-constraint-file-editor/NamingConstraintFileEditor.css
@@ -0,0 +1,20 @@
+.naming-constraint-file-editor {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 6px 12px;
+ flex: 1;
+ .naming-constraint-file-editor__field {
+ min-width: 0;
+ font-size: 12px;
+ display: flex;
+ flex-direction: column;
+ justify-content: start;
+ &:deep(.bimdata-select), &:deep(.bimdata-input) {
+ margin: 12px 0px 6px !important;
+ }
+ span {
+ font-size: 11px;
+ color: var(--color-granite-light);
+ }
+ }
+}
diff --git a/src/components/specific/files/naming-constraint/conflicting-document-item/naming-constraint-file-editor/NamingConstraintFileEditor.vue b/src/components/specific/files/naming-constraint/conflicting-document-item/naming-constraint-file-editor/NamingConstraintFileEditor.vue
new file mode 100644
index 000000000..23b1c412a
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/conflicting-document-item/naming-constraint-file-editor/NamingConstraintFileEditor.vue
@@ -0,0 +1,164 @@
+
+
+
+
+
+
+
+
+
+
+ {{ $t("NamingConstraint.maxCharsHint", { count: part.max_value }) }}
+
+
+
+
+
+
+ {{ $t("NamingConstraint.maxCharsHint", { count: part.max_length }) }}
+
+
+
+
+
+
+
+
+
diff --git a/src/components/specific/files/naming-constraint/conflicting-documents-list/ConflictingDocumentsList.css b/src/components/specific/files/naming-constraint/conflicting-documents-list/ConflictingDocumentsList.css
new file mode 100644
index 000000000..f2f75c0df
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/conflicting-documents-list/ConflictingDocumentsList.css
@@ -0,0 +1,107 @@
+.conflicting-documents-list {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ max-height: 450px;
+ margin: 0;
+ padding: 0;
+ overflow-y: auto;
+ list-style: none;
+}
+
+.conflicting-documents-list__group {
+ display: flex;
+ flex-direction: column;
+ border: 1px solid var(--color-silver);
+ border-radius: 8px;
+ background-color: #f8f9fa;
+}
+
+.conflicting-documents-list__group__header {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--color-silver);
+}
+
+.conflicting-documents-list__group__label {
+ color: var(--color-granite-light);
+ font-size: 11px;
+ font-weight: 600;
+ line-height: 14px;
+ text-transform: uppercase;
+}
+
+.conflicting-documents-list__group > ul,
+.conflicting-documents-list__ungrouped > ul {
+ display: flex;
+ flex-direction: column;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.conflicting-documents-list__group .conflicting-documents-list__item {
+ border-bottom: 1px solid var(--color-silver);
+ background: var(--color-silver-light);
+}
+
+.conflicting-documents-list__group .conflicting-documents-list__item:last-child {
+ border-bottom: none;
+}
+
+.conflicting-documents-list__ungrouped > ul {
+ gap: 8px;
+}
+
+.conflicting-documents-list__ungrouped .conflicting-documents-list__item {
+ background-color: var(--color-silver-light);
+ border: 1px solid var(--color-silver);
+ border-radius: 8px;
+}
+
+.conflicting-documents-list__item__name {
+ flex: 1;
+ overflow: hidden;
+ color: var(--color-primary);
+ font-size: 13px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.conflicting-documents-list__item__info {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.conflicting-documents-list__item__example {
+ overflow: hidden;
+ color: var(--color-granite-light);
+ font-size: 11px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.conflicting-documents-list__item__path {
+ font-size: 11px;
+}
+
+.conflicting-documents-list__item__input {
+ flex: 1;
+}
+
+.conflicting-documents-list__item--editing {
+ background-color: var(--color-white);
+}
+
+.conflicting-documents-list__item--invalid .conflicting-documents-list__item__name {
+ color: var(--color-high);
+}
+
+.conflicting-documents-list__item--deleted .conflicting-documents-list__item__name {
+ color: var(--color-granite-light);
+ text-decoration: line-through;
+}
diff --git a/src/components/specific/files/naming-constraint/conflicting-documents-list/ConflictingDocumentsList.vue b/src/components/specific/files/naming-constraint/conflicting-documents-list/ConflictingDocumentsList.vue
new file mode 100644
index 000000000..2f55cd4dd
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/conflicting-documents-list/ConflictingDocumentsList.vue
@@ -0,0 +1,174 @@
+
+
+
+
+
+
+
diff --git a/src/components/specific/files/naming-constraint/folder-naming-constraint-selector/FolderNamingConstraintSelector.css b/src/components/specific/files/naming-constraint/folder-naming-constraint-selector/FolderNamingConstraintSelector.css
new file mode 100644
index 000000000..e74aa8893
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/folder-naming-constraint-selector/FolderNamingConstraintSelector.css
@@ -0,0 +1,145 @@
+.folder-naming-constraint-selector {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ height: 100%;
+}
+.folder-naming-constraint-selector__head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+.folder-naming-constraint-selector__head__title {
+ font-size: 16px;
+ color: var(--color-granite);
+ strong {
+ color: var(--color-primary);
+ }
+}
+.folder-naming-constraint-selector__empty {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 12px;
+ padding: 24px 12px;
+ text-align: center;
+}
+.folder-naming-constraint-selector__empty__title {
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--color-primary);
+}
+.folder-naming-constraint-selector__empty__text {
+ max-width: 280px;
+ font-size: 13px;
+ line-height: 18px;
+ color: var(--color-granite);
+}
+.folder-naming-constraint-selector__head__actions {
+ gap: calc(var(--spacing-unit) / 2);
+ width: 100%;
+ .folder-naming-constraint-selector__search {
+ min-height: 32px;
+ }
+ .bimdata-btn {
+ flex: 1;
+ }
+}
+.folder-naming-constraint-selector__items {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ margin: 0;
+ padding: 6px 6px 12px 6px;
+ list-style: none;
+ overflow-y: auto;
+}
+.folder-naming-constraint-selector__item {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: var(--spacing-unit) calc(var(--spacing-unit) / 2);
+ border: 1px solid transparent;
+ border-radius: 6px;
+ background-color: var(--color-white);
+ border: 1px solid #eceff3;
+ cursor: pointer;
+}
+.folder-naming-constraint-selector__item--selected {
+ border-color: var(--color-primary);
+}
+.folder-naming-constraint-selector__item__main {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ min-width: 0;
+ flex-grow: 1;
+}
+.folder-naming-constraint-selector__item__name {
+ max-width: 75%;
+ flex: 1;
+ font-weight: 600;
+ color: var(--color-primary);
+}
+.folder-naming-constraint-selector__item__badges {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+}
+.folder-naming-constraint-selector__item__chip {
+ padding: 2px 6px;
+ border-radius: 6px;
+ background-color: #F0F5FF;
+ color: var(--color-neutral);
+ font-family: monospace;
+ font-size: 12px;
+}
+.folder-naming-constraint-selector__item__preview {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 3px;
+}
+
+.folder-naming-constraint-selector__item__separator, .folder-naming-constraint-selector__item__extension {
+ color: #64748b;
+ font-weight: 700;
+}
+
+.folder-naming-constraint-selector__item__chip--strict {
+ background-color: var(--color-secondary-lighter);
+ color: var(--color-warning);
+ font-family: inherit;
+}
+
+
+.folder-naming-constraint-selector__inheritance-info {
+ gap: 6px;
+ padding: var(--spacing-unit);
+ border-radius: 6px;
+ background-color: var(--color-primary-lighter);
+ font-size: 13px;
+ p {
+ margin: 0;
+ font-size: 12px;
+ color: var(--color-granite);
+ }
+}
+
+.folder-naming-constraint-selector__footer {
+ margin-top: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+.folder-naming-constraint-selector__footer__help {
+ font-size: 12px;
+ line-height: 17px;
+ color: var(--color-granite);
+}
+.folder-naming-constraint-selector__footer__save {
+ margin-top: 6px;
+}
\ No newline at end of file
diff --git a/src/components/specific/files/naming-constraint/folder-naming-constraint-selector/FolderNamingConstraintSelector.vue b/src/components/specific/files/naming-constraint/folder-naming-constraint-selector/FolderNamingConstraintSelector.vue
new file mode 100644
index 000000000..65cebf693
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/folder-naming-constraint-selector/FolderNamingConstraintSelector.vue
@@ -0,0 +1,464 @@
+
+
+
+
+ {{ $t("FolderNamingConstraint.ruleSectionTitle") }}
+ {{ localState.folder.name }}
+
+
+
+
+
+
+
+ {{ $t("NamingConstraint.addRuleButton") }}
+
+
+
+
+
+
+ {{ $t("NamingConstraint.rulesEmptyTitle") }}
+
+
+ {{ $t("NamingConstraint.rulesEmptyPrerequisitesText") }}
+
+
+ {{ $t("NamingConstraint.rulesEmptyText") }}
+
+
+
+ {{ $t("NamingConstraint.addRuleButton") }}
+
+
+
+
+
+ -
+
+
+
+ {{ $t("NamingConstraint.noDirectRuleOption") }}
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+ {{ $t("NamingConstraint.strictBadge") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t("NamingConstraint.inheritedRuleTitle") }}
+
+
+ {{
+ $t("NamingConstraint.inheritedRuleDescription", {
+ rule: inheritedRule.name,
+ folder: inheritedRule.folderName,
+ })
+ }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/specific/files/naming-constraint/naming-constraint-form/NamingConstraintForm.css b/src/components/specific/files/naming-constraint/naming-constraint-form/NamingConstraintForm.css
new file mode 100644
index 000000000..82b16d631
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/naming-constraint-form/NamingConstraintForm.css
@@ -0,0 +1,97 @@
+.naming-constraint-form {
+ height: calc(100% - 18px);
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ gap: 12px;
+}
+.naming-constraint-form__body {
+ height: calc(100% - 32px - 12px);
+ overflow: auto;
+}
+.naming-constraint-form__title {
+ font-size: 16px;
+ font-weight: 600;
+ text-align: center;
+ color: var(--color-primary);
+}
+.naming-constraint-form__content {
+ height: calc(100% - 22px - 12px);
+ overflow: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ padding: 0 6px 6px 0;
+ .naming-constraint-form__step {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ padding: 12px;
+ border-radius: 8px;
+ background-color: var(--color-silver-light);
+ }
+ .naming-constraint-form__step__head {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+ .naming-constraint-form__step__num {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background-color: var(--color-primary);
+ color: var(--color-white);
+ font-size: 12px;
+ }
+ .naming-constraint-form__step__label {
+ font-weight: 600;
+ color: var(--color-primary);
+ }
+ .naming-constraint-form__radios {
+ display: flex;
+ gap: var(--spacing-unit);
+ .bimdata-radio {
+ flex: auto;
+ }
+ }
+ .naming-constraint-form__strict {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ }
+ .naming-constraint-form__help {
+ font-size: 12px;
+ line-height: 17px;
+ color: var(--color-granite);
+ }
+ .naming-constraint-form__error {
+ font-size: 12px;
+ color: var(--color-high);
+ }
+ .naming-constraint-form__preview {
+ gap: 6px;
+ padding: 8px 12px;
+ border-radius: 6px;
+ background-color: var(--color-neutral-lighter);
+ }
+ .naming-constraint-form__preview__label {
+ font-size: 12px;
+ color: var(--color-granite);
+ }
+ .naming-constraint-form__preview__value {
+ font-family: monospace;
+ font-size: 13px;
+ color: var(--color-neutral);
+ }
+}
+.naming-constraint-form__actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 12px;
+ .bimdata-btn {
+ flex: auto;
+ }
+}
\ No newline at end of file
diff --git a/src/components/specific/files/naming-constraint/naming-constraint-form/NamingConstraintForm.vue b/src/components/specific/files/naming-constraint/naming-constraint-form/NamingConstraintForm.vue
new file mode 100644
index 000000000..3e100e2d5
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/naming-constraint-form/NamingConstraintForm.vue
@@ -0,0 +1,431 @@
+
+
+
+
+
+
+
diff --git a/src/components/specific/files/naming-constraint/naming-constraint-form/RuleBuilder.css b/src/components/specific/files/naming-constraint/naming-constraint-form/RuleBuilder.css
new file mode 100644
index 000000000..51678bf03
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/naming-constraint-form/RuleBuilder.css
@@ -0,0 +1,225 @@
+.rule-builder {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+.rule-builder__empty-text {
+ margin: 0;
+ color: var(--color-granite);
+ font-size: 13px;
+}
+.rule-builder__add-label {
+ margin: 0;
+ color: var(--color-granite-light);
+ font-size: 10px;
+}
+.rule-builder__add-buttons {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ .rule-builder__add-button {
+ flex: auto;
+ }
+}
+
+.rule-builder__parts {
+ position: relative;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.rule-builder__part {
+ position: relative;
+ border-radius: 6px;
+ background-color: var(--color-white);
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
+ transition: box-shadow 0.15s ease, opacity 0.15s ease;
+
+ &.rule-builder__part--dragging {
+ opacity: 0.4;
+ }
+
+ &.rule-builder__part--drag-over-top::before,
+ &.rule-builder__part--drag-over-bottom::after {
+ content: "";
+ position: absolute;
+ left: 12px;
+ right: 12px;
+ height: 1px;
+ background-color: var(--color-primary);
+ border-radius: 1px;
+ }
+ &.rule-builder__part--drag-over-top::before {
+ top: 0;
+ }
+ &.rule-builder__part--drag-over-bottom::after {
+ bottom: 0;
+ }
+
+ .rule-builder__part__main {
+ padding: 6px;
+ gap: 3px;
+ }
+
+ .rule-builder__part__grip {
+ cursor: grab;
+ color: var(--color-granite-light);
+ display: flex;
+ align-items: center;
+ }
+ .rule-builder__part__grip:active {
+ cursor: grabbing;
+ }
+
+ .rule-builder__part__type {
+ flex-shrink: 0;
+ min-width: 90px;
+ font-size: 12px;
+ color: var(--color-primary);
+ }
+
+ .rule-builder__part__value {
+ flex: 1 1 auto;
+ width: 140px;
+ min-width: 140px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ }
+ .rule-builder__part__value__empty {
+ padding: 0 6px;
+ color: var(--color-granite-light);
+ text-transform: uppercase;
+ font-size: 10px;
+ font-weight: 800;
+ }
+
+ .rule-builder__part__actions {
+ width: 64px;
+ display: flex;
+ align-items: center;
+ justify-content: end;
+ }
+
+ .rule-builder__part__description {
+ padding: 3px var(--spacing-unit) calc(var(--spacing-unit) / 2) var(--spacing-unit);
+ width: calc(100% - (90px + 18px));
+ display: flex;
+ justify-self: flex-end;
+ .bimdata-input-outlined {
+ &:deep(fieldset) {
+ label {
+ margin-bottom: 3px;
+ font-size: 9px;
+ text-transform: none;
+ font-weight: 400;
+ }
+ }
+ &:deep(.bimdata-input-outlined__container){
+ height: 30px;
+ min-height: 30px;
+ input {
+ min-height: auto;
+ font-size: 11px;
+ }
+ }
+ }
+ }
+}
+
+/* --- Champs --- */
+.rule-builder__number {
+ flex: 1;
+ height: 30px;
+ min-width: 0;
+ padding: 0 8px;
+ border: 1px solid var(--color-silver);
+ border-radius: 6px;
+ background-color: var(--color-white);
+ color: var(--color-primary);
+ font-size: 13px;
+ transition: all 0.15s ease;
+}
+.rule-builder__number:hover {
+ border-color: var(--color-silver-dark);
+ transition: all 0.15s ease;
+}
+.rule-builder__number:focus {
+ outline: none;
+}
+
+.rule-builder__dash {
+ flex-shrink: 0;
+ color: var(--color-granite);
+}
+
+/* --- Select --- */
+.rule-builder__select {
+ flex: 1;
+ &__label {
+ max-width: 85%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ &:deep(.bimdata-dropdown__content) {
+ padding: 0 6px;
+ font-size: 11px;
+ max-width: 140px;
+ overflow: hidden;
+ svg {
+ min-height: 11px!important;
+ height: 11px!important;
+ width: 11px!important;
+ min-width: 11px!important;
+ }
+ }
+}
+.rule-builder__select:hover {
+ border-color: var(--color-silver-dark);
+ transition: all 0.15s ease;
+}
+.rule-builder__select:focus {
+ outline: none;
+}
+
+/* --- Bouton "créer une liste" --- */
+.rule-builder__create-list-btn--active {
+ background-color: var(--color-primary);
+ color: var(--color-white);
+ &:hover {
+ background-color: var(--color-primary-light);
+ }
+}
+
+/* --- Panneau de création inline --- */
+.rule-builder__create-panel {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin: 0 14px 12px 14px;
+ padding: 10px 12px;
+ border-radius: 6px;
+ background-color: var(--color-silver-light, #f5f6f8);
+}
+.rule-builder__create-panel__input {
+ height: 34px;
+ padding: 0 10px;
+ border: 1px solid var(--color-silver);
+ border-radius: 6px;
+ background-color: var(--color-white);
+ font-size: 13px;
+ color: var(--color-primary);
+}
+.rule-builder__create-panel__input:focus {
+ outline: none;
+ border-color: var(--color-primary);
+}
+.rule-builder__create-panel__actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+}
\ No newline at end of file
diff --git a/src/components/specific/files/naming-constraint/naming-constraint-form/RuleBuilder.vue b/src/components/specific/files/naming-constraint/naming-constraint-form/RuleBuilder.vue
new file mode 100644
index 000000000..c9999f840
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/naming-constraint-form/RuleBuilder.vue
@@ -0,0 +1,384 @@
+
+
+
+
+
+ {{ $t("NamingConstraint.emptyStateHelp") }}
+
+
+
+ {{ $t("NamingConstraint.addPartButton") }}
+
+
+
+
+ + {{ $t("NamingConstraint.partTypeNChars") }}
+
+
+
+ + {{ $t("NamingConstraint.partTypeBounded") }}
+
+
+
+ + {{ $t("NamingConstraint.partTypeValuesIn") }}
+
+
+
+
+
+
+
+
diff --git a/src/components/specific/files/naming-constraint/naming-constraint-preview/NamingConstraintPreview.css b/src/components/specific/files/naming-constraint/naming-constraint-preview/NamingConstraintPreview.css
new file mode 100644
index 000000000..ae6375b31
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/naming-constraint-preview/NamingConstraintPreview.css
@@ -0,0 +1,20 @@
+.naming-constraint-preview {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 1px;
+}
+
+.naming-constraint-preview__chip {
+ padding: 2px 6px;
+ border-radius: 6px;
+ background-color: #F0F5FF;
+ color: var(--color-neutral);
+ font-family: monospace;
+ font-size: 12px;
+}
+
+.naming-constraint-preview__separator, .naming-constraint-preview__extension {
+ color: #64748b;
+ font-weight: 700;
+}
diff --git a/src/components/specific/files/naming-constraint/naming-constraint-preview/NamingConstraintPreview.vue b/src/components/specific/files/naming-constraint/naming-constraint-preview/NamingConstraintPreview.vue
new file mode 100644
index 000000000..f40e4d658
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/naming-constraint-preview/NamingConstraintPreview.vue
@@ -0,0 +1,50 @@
+
+
+
+
+ {{ part }}
+
+
+
+ {{ separator }}
+
+
+
+ .ext
+
+
+
+
+
+
diff --git a/src/components/specific/files/naming-constraint/naming-constraints-list/NamingConstraintsList.css b/src/components/specific/files/naming-constraint/naming-constraints-list/NamingConstraintsList.css
new file mode 100644
index 000000000..8f280413e
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/naming-constraints-list/NamingConstraintsList.css
@@ -0,0 +1,141 @@
+.naming-constraints-list {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+.naming-constraints-list__head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+.naming-constraints-list__head__title {
+ font-size: 16px;
+ font-weight: 500;
+ color: var(--color-primary);
+}
+.naming-constraints-list__head__actions {
+ gap: var(--spacing-unit);
+ width: 100%;
+ .bimdata-btn {
+ flex: 1;
+ }
+}
+.naming-constraints-list__empty {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 12px;
+ padding: 24px 12px;
+ text-align: center;
+}
+.naming-constraints-list__empty__title {
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--color-primary);
+}
+.naming-constraints-list__empty__text {
+ max-width: 280px;
+ font-size: 13px;
+ color: var(--color-granite);
+}
+.naming-constraints-list__empty__warning {
+ padding: var(--spacing-unit) calc(var(--spacing-unit) * 1.5);
+ gap: 6px;
+ background-color: var(--color-warning-lighter);
+ border-radius: 8px;
+ font-size: 12px;
+ line-height: 18px;
+ color: var(--color-granite);
+}
+.naming-constraints-list__search {
+ min-height: 32px;
+}
+.naming-constraints-list__items {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin: 0;
+ padding: 6px;
+ list-style: none;
+ overflow-y: auto;
+}
+.naming-constraints-list__item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px;
+ border-radius: 6px;
+ background-color: var(--color-white);
+ box-shadow: var(--box-shadow);
+}
+.naming-constraints-list__item__main {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ min-width: 0;
+ flex-grow: 1;
+}
+.naming-constraints-list__item__name {
+ .bimdata-textbox {
+ color: var(--color-primary);
+ font-weight: 600;
+ }
+}
+.naming-constraints-list__item__badges {
+ flex-wrap: wrap;
+ gap: 6px;
+}
+.naming-constraints-list__item__avatar {
+ padding: 2px 4px;
+ border-radius: 6px;
+ background-color: #F0F5FF;
+ color: var(--color-neutral);
+ font-size: 12px;
+ &:deep(.user-avatar ) {
+ min-width: 16px;
+ min-height: 16px;
+ }
+}
+.naming-constraints-list__item__preview {
+ gap: 6px;
+ flex-wrap: wrap;
+}
+.naming-constraints-list__item__chip {
+ padding: 2px 8px;
+ border-radius: 6px;
+ background-color: #F0F5FF;
+ color: var(--color-neutral);
+ font-family: monospace;
+ font-size: 12px;
+}
+.naming-constraints-list__item__separator, .naming-constraints-list__item__extension {
+ color: #64748b;
+ font-weight: 600;
+}
+
+.naming-constraints-list__item__chip--strict {
+ background-color: var(--color-secondary-lighter);
+ color: var(--color-warning);
+ font-family: inherit;
+}
+.naming-constraints-list__item__actions {
+ display: flex;
+ align-items: center;
+ flex-shrink: 0;
+ .delete-actions {
+ position: absolute;
+ right: 9px;
+ background-color: var(--color-white);
+ gap: 3px;
+ z-index: 1;
+ }
+}
+.naming-constraints-list__manage {
+ margin-top: auto;
+ align-self: flex-start;
+}
\ No newline at end of file
diff --git a/src/components/specific/files/naming-constraint/naming-constraints-list/NamingConstraintsList.vue b/src/components/specific/files/naming-constraint/naming-constraints-list/NamingConstraintsList.vue
new file mode 100644
index 000000000..06f9c56e9
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/naming-constraints-list/NamingConstraintsList.vue
@@ -0,0 +1,202 @@
+
+
+
+
+ {{ $t("NamingConstraint.rulesSectionTitle") }}
+
+
+
+ {{ $t("NamingConstraint.addRuleButton") }}
+
+
+
+
+
+
+
+
+ {{ $t("NamingConstraint.rulesEmptyTitle") }}
+
+
+ {{ $t("NamingConstraint.rulesEmptyText") }}
+
+
+
+
+ {{ $t("NamingConstraint.rulesEmptyPrerequisitesText") }}
+
+
+
+
+ {{ $t("NamingConstraint.addRuleButton") }}
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ {{ $t("NamingConstraint.strictBadge") }}
+
+
+
+
+
+
+
+
+ {{ $t("t.confirm") }}
+
+
+ {{ $t("t.cancel") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ constraint?.creator?.email }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/specific/files/naming-constraint/naming-part-list-form/NamingPartListForm.css b/src/components/specific/files/naming-constraint/naming-part-list-form/NamingPartListForm.css
new file mode 100644
index 000000000..258c5ae27
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/naming-part-list-form/NamingPartListForm.css
@@ -0,0 +1,97 @@
+.naming-part-list-form__container {
+ height: 100%;
+}
+.naming-part-list-form {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ padding: 18px 0;
+}
+.naming-part-list-form__header {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+}
+.naming-part-list-form__header__title {
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--color-primary);
+}
+.naming-part-list-form__step {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 18px;
+ padding: 14px;
+ border-radius: 8px;
+ background-color: var(--color-silver-light);
+}
+.naming-part-list-form__step__head {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.naming-part-list-form__step__num {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background-color: var(--color-primary);
+ color: var(--color-white);
+ font-size: 12px;
+}
+.naming-part-list-form__step__label {
+ font-weight: 600;
+ color: var(--color-primary);
+}
+.naming-part-list-form__element {
+ display: flex;
+ align-items: flex-end;
+ gap: 12px;
+ .bimdata-input {
+ flex: 1;
+ }
+}
+.naming-part-list-form__element__label {
+ font-size: 14px;
+ font-weight: 700;
+ color: var(--color-primary);
+}
+.naming-part-list-form__bulk-import {
+ .naming-part-list-form__bulk-import__toggle {
+ font-size: 14px;
+ font-weight: 700;
+ color: var(--color-primary);
+ cursor: pointer;
+ }
+ .naming-part-list-form__bulk-import__hint {
+ color: var(--color-granite-light);
+ font-size: 11px;
+ }
+ &:deep(.bimdata-textarea) {
+ margin-bottom: 0;
+ textarea {
+ padding: 6px;
+ font-size: 12px;
+ }
+ }
+}
+
+.naming-part-list-form__add {
+ align-self: flex-start;
+}
+.naming-part-list-form__error {
+ font-size: 12px;
+ color: var(--color-high);
+}
+.naming-part-list-form__actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 12px;
+ .bimdata-btn {
+ flex: 1;
+ }
+}
diff --git a/src/components/specific/files/naming-constraint/naming-part-list-form/NamingPartListForm.vue b/src/components/specific/files/naming-constraint/naming-part-list-form/NamingPartListForm.vue
new file mode 100644
index 000000000..aa3e586a9
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/naming-part-list-form/NamingPartListForm.vue
@@ -0,0 +1,399 @@
+
+
+
+
+
+
+
diff --git a/src/components/specific/files/naming-constraint/naming-part-lists-list/NamingPartListsList.css b/src/components/specific/files/naming-constraint/naming-part-lists-list/NamingPartListsList.css
new file mode 100644
index 000000000..f2c52aa24
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/naming-part-lists-list/NamingPartListsList.css
@@ -0,0 +1,92 @@
+.naming-part-lists-list {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+.naming-part-lists-list__head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ .naming-part-lists-list__head__title {
+ font-size: 16px;
+ font-weight: 500;
+ color: var(--color-primary);
+ }
+}
+.naming-part-lists-list__search {
+ min-height: 32px;
+}
+.naming-part-lists-list__empty {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 12px;
+ padding: 24px 12px;
+ text-align: center;
+ .naming-part-lists-list__empty__title {
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--color-primary);
+ }
+ .naming-part-lists-list__empty__text {
+ max-width: 280px;
+ font-size: 13px;
+ line-height: 18px;
+ color: var(--color-granite);
+ }
+}
+.naming-part-lists-list__items {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin: 0;
+ padding: 6px;
+ list-style: none;
+ overflow: auto;
+}
+.naming-part-lists-list__item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px;
+ border-radius: 6px;
+ background-color: var(--color-white);
+ box-shadow: var(--box-shadow);
+}
+.naming-part-lists-list__item__main {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ min-width: 0;
+ flex-grow: 1;
+}
+.naming-part-lists-list__item__name {
+ font-weight: 600;
+ color: var(--color-primary);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.naming-part-lists-list__item__badges {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+}
+.naming-part-lists-list__item__chip {
+ padding: 2px 8px;
+ border-radius: 6px;
+ background-color: var(--color-neutral-lighter);
+ color: var(--color-neutral);
+ font-size: 12px;
+}
+.naming-part-lists-list__item__actions {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ flex-shrink: 0;
+}
diff --git a/src/components/specific/files/naming-constraint/naming-part-lists-list/NamingPartListsList.vue b/src/components/specific/files/naming-constraint/naming-part-lists-list/NamingPartListsList.vue
new file mode 100644
index 000000000..3e9863710
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/naming-part-lists-list/NamingPartListsList.vue
@@ -0,0 +1,202 @@
+
+
+
+
+ {{ $t("NamingConstraint.listsSectionTitle") }}
+
+
+
+ {{ $t("NamingConstraint.createListButton") }}
+
+
+
+
+
+
+
+
+ {{ $t("NamingConstraint.listsEmptyTitle") }}
+
+
+ {{ $t("NamingConstraint.listsEmptyText") }}
+
+
+
+ {{ $t("NamingConstraint.createListButton") }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/specific/files/naming-constraint/naming-part-lists-list/naming-part-list-in-use-modal/NamingPartListInUseModal.css b/src/components/specific/files/naming-constraint/naming-part-lists-list/naming-part-list-in-use-modal/NamingPartListInUseModal.css
new file mode 100644
index 000000000..85417504f
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/naming-part-lists-list/naming-part-list-in-use-modal/NamingPartListInUseModal.css
@@ -0,0 +1,33 @@
+.naming-part-list-in-use-modal {
+ padding: 0 var(--spacing-unit);
+}
+.naming-part-list-in-use-modal__content {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ text-align: left;
+ font-size: 13px;
+}
+
+.naming-part-list-in-use-modal__content p {
+ margin: 0;
+}
+
+.naming-part-list-in-use-modal__rules {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin: 0;
+ padding: 6px 12px;
+ li {
+ list-style-type: decimal;
+ overflow-wrap: anywhere;
+ }
+}
+
+.naming-part-list-in-use-modal__hint {
+ padding: 6px 12px;
+ background-color: var(--color-silver-light);
+ color: var(--color-granite-light);
+ border-radius: 6px;
+}
diff --git a/src/components/specific/files/naming-constraint/naming-part-lists-list/naming-part-list-in-use-modal/NamingPartListInUseModal.vue b/src/components/specific/files/naming-constraint/naming-part-lists-list/naming-part-list-in-use-modal/NamingPartListInUseModal.vue
new file mode 100644
index 000000000..0b9854980
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/naming-part-lists-list/naming-part-list-in-use-modal/NamingPartListInUseModal.vue
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+ {{ $t("NamingConstraint.partListInUseModal.listLabel") }}
+ {{ partList?.name }}
+
+
+
+
+ {{ $t("NamingConstraint.partListInUseModal.rulesDescription") }}
+
+
+ -
+ {{ constraint.name }}
+
+
+
+
+
+ {{ $t("NamingConstraint.partListInUseModal.resolution") }}
+
+
+
+
+
+
+ {{ $t("t.close") }}
+
+
+
+
+
+
+
+
diff --git a/src/config/naming-constraints.js b/src/config/naming-constraints.js
new file mode 100644
index 000000000..9cecae210
--- /dev/null
+++ b/src/config/naming-constraints.js
@@ -0,0 +1 @@
+export const NONE_OPTION_ID = "__none__";
diff --git a/src/i18n/lang/de.json b/src/i18n/lang/de.json
index d12b95fb7..3ec58f7b4 100644
--- a/src/i18n/lang/de.json
+++ b/src/i18n/lang/de.json
@@ -1,4 +1,35 @@
{
+ "NamingConstraint": {
+ "managerTitle": "Verwaltung der Namenskonventionen",
+ "constraintsTab": "Benennungsregeln",
+ "strictBadge": "Strikt",
+ "createConstraintTitle": "Regel erstellen",
+ "updateConstraintTitle": "Regel bearbeiten",
+ "createListTitle": "Meine Liste erstellen",
+ "updateListTitle": "Meine Liste bearbeiten",
+ "nameLabel": "Name",
+ "strictLabel": "Strikte Regel",
+ "strictHelp": "Wenn Sie dieses Kästchen aktivieren, müssen Sie alle Dateien umbenennen, die nicht dieser neuen Regel entsprechen.",
+ "previewLabel": "Vorschau",
+ "ruleSectionTitle": "Regel",
+ "addPartButton": "Element hinzufügen",
+ "emptyParts": "Fügen Sie mindestens ein Element hinzu, um die Regel zu definieren.",
+ "partTypeLabel": "Typ",
+ "partTypeValuesIn": "Liste",
+ "partTypeBounded": "Begrenzte Werte",
+ "partTypeNChars": "N Zeichen",
+ "elementsHelp": "Kommagetrennte Liste zulässiger Werte.",
+ "minLabel": "Min",
+ "maxLabel": "Max",
+ "maxLengthLabel": "Max. Länge",
+ "emptyElementsError": "Geben Sie mindestens einen Wert an.",
+ "invalidBoundsError": "Max muss größer oder gleich Min sein.",
+ "missingBoundsError": "Min und Max sind für begrenzte Werte erforderlich.",
+ "invalidMaxLengthError": "Die maximale Länge muss mindestens 1 betragen.",
+ "missingListError": "Wählen Sie für jedes Listenelement eine Liste aus.",
+ "listInUseErrorTitle": "Liste in Verwendung",
+ "listInUseErrorMessage": "Diese Liste wird von den folgenden Regeln verwendet: {constraints}. Bearbeiten Sie diese, bevor Sie die Liste löschen."
+ },
"OidcCallbackError": {
"message": "Bei der Authentifizierung ist ein Fehler aufgetreten...",
"retryButtonText": "Nochmals versuchen"
@@ -442,6 +473,19 @@
"invitationViewAcceptError": "Die Einladung kann nicht angenommen werden",
"folderFetchFolder": "Fehler beim Abrufen von Ordnerinformationen.",
"bcfDeleteError": "Das Löschen ist fehlgeschlagen...",
+ "namingConstraintsFetchError": "Benennungsregeln konnten nicht abgerufen werden",
+ "namingConstraintFetchError": "Diese Benennungsregel konnte nicht abgerufen werden",
+ "namingConstraintCreateError": "Diese Benennungsregel konnte nicht erstellt werden",
+ "namingConstraintUpdateError": "Diese Benennungsregel konnte nicht aktualisiert werden",
+ "namingConstraintDeleteError": "Diese Benennungsregel konnte nicht gelöscht werden",
+ "namingPartListsFetchError": "Benennungslisten konnten nicht abgerufen werden",
+ "namingPartListCreateError": "Diese Benennungsliste konnte nicht erstellt werden",
+ "namingPartListUpdateError": "Diese Benennungsliste konnte nicht aktualisiert werden",
+ "namingPartListDeleteError": "Diese Benennungsliste konnte nicht gelöscht werden",
+ "folderNamingConstraintFetchError": "Die Benennungsregel des Ordners konnte nicht abgerufen werden",
+ "folderNamingConstraintSetError": "Die Benennungsregel konnte nicht auf diesen Ordner angewendet werden",
+ "folderNamingConstraintDeleteError": "Die Benennungsregel konnte nicht von diesem Ordner entfernt werden",
+ "conflictingDocumentsFetchError": "Dokumente mit Konflikten konnten nicht abgerufen werden",
"groupImportError": "Fehler beim Importieren von Gruppen",
"bcfExportXlsxError": "Der Excel-Export ist fehlgeschlagen...",
"invitationViewDeclineError": "Einladung kann nicht abgelehnt werden"
@@ -973,4 +1017,4 @@
"title": "Löschen der {visasCount} Freigaben",
"message": "Sie sind dabei, die Freigaben für die folgenden Dateien zu löschen:"
}
-}
\ No newline at end of file
+}
diff --git a/src/i18n/lang/en.json b/src/i18n/lang/en.json
index 34c903bab..628c3fc75 100644
--- a/src/i18n/lang/en.json
+++ b/src/i18n/lang/en.json
@@ -1,4 +1,123 @@
{
+ "NamingConstraint": {
+ "managerTitle": "Naming convention management",
+ "constraintsTab": "Naming rules",
+ "rulesSectionTitle": "Rules list",
+ "addRuleButton": "add a rule",
+ "searchConstraintPlaceholder": "Search for a rule",
+ "searchListPlaceholder": "Search for a list",
+ "rulesEmptyTitle": "No rule added",
+ "rulesEmptyText": "You don't have any rule yet, start by adding one.",
+ "rulesEmptyPrerequisitesText": "Naming rules only apply to files inside a folder.",
+ "manageListsButton": "Manage lists",
+ "listsSectionTitle": "Lists management",
+ "createListButton": "Create a list",
+ "listsEmptyTitle": "No list added",
+ "listsEmptyText": "You don't have any list yet, start by creating one.",
+ "strictBadge": "Strict",
+ "createConstraintTitle": "Creating a rule",
+ "updateConstraintTitle": "Editing a rule",
+ "createListTitle": "Creating my list",
+ "updateListTitle": "Editing my list",
+ "nameLabel": "Name",
+ "ruleNameStep": "Rule name",
+ "ruleNamePlaceholder": "Rule name",
+ "separatorStep": "Separator type",
+ "separatorDashOption": "- (dash)",
+ "separatorDotOption": ". (dot)",
+ "separatorUnderscoreOption": "_ (underscore)",
+ "structureSectionTitle": "Rule structure",
+ "strictLabel": "Strict rule",
+ "strictHelp": "If you check this box, you will have to rename every file that does not match this new rule.",
+ "previewLabel": "Preview",
+ "ruleSectionTitle": "Rule",
+ "addPartButton": "Add an element",
+ "emptyParts": "Add at least one element to define the rule.",
+ "partTypeLabel": "Type",
+ "partTypeValuesIn": "List",
+ "partTypeBounded": "Bounded values",
+ "partTypeNChars": "N characters",
+ "selectListPlaceholder": "Select a list",
+ "noList": "No list created",
+ "elementsHelp": "Comma-separated list of allowed values.",
+ "minLabel": "Min",
+ "maxLabel": "Max",
+ "maxLengthLabel": "Max length",
+ "partNameLabel": "Field name (optional)",
+ "partNamePlaceholder": "Enter a name",
+ "saveRuleButton": "Save the rule",
+ "listNameStep": "List name",
+ "listNamePlaceholder": "List name",
+ "addElementsStep": "Add elements",
+ "addOneElementLabel": "Add an element",
+ "elementNamePlaceholder": "Element name",
+ "removeElementButton": "Remove",
+ "addElementButton": "Add an element",
+ "bulkImportLabel": "Add multiple elements",
+ "bulkImportPlaceholder": "e.g.: PLAN, VISA, DOE",
+ "bulkImportHint": "Enter your elements separated by commas",
+ "importButton": "Generate the list",
+ "saveListButton": "Save the list",
+ "emptyElementsError": "Provide at least one value.",
+ "invalidBoundsError": "Max must be greater than or equal to min.",
+ "missingBoundsError": "Min and max are required for bounded values.",
+ "invalidMaxLengthError": "Max length must be at least 1.",
+ "missingListError": "Select a list for every list element.",
+ "listInUseErrorTitle": "List in use",
+ "listInUseErrorMessage": "This list is used by the following rules: {constraints}. Edit them before deleting the list.",
+ "folderRuleMenuItem": "Naming convention",
+ "recursiveLabel": "Include subfolders",
+ "recursiveHelp": "If you check this box, this rule will also apply to files contained in the subfolders of your main folder.",
+ "applyRuleSuccessTitle": "Naming rule applied",
+ "applyRuleSuccessMessage": "The naming convention has been applied to the folder \"{folder}\".",
+ "applyRuleError": "The naming convention could not be applied.",
+ "applyRuleWarningTitle": "Name does not match the rule",
+ "conflictTooltip": "This file does not match the folder naming convention.",
+ "conflictModalIntro": "Rename here the files that do not match the strict rule you set.",
+ "conflictModalWarning": "Rename or delete the conflicting documents to continue",
+ "conflictCurrentNameLabel": "Current name",
+ "conflictNewNameLabel": "New name",
+ "conflictStatusDeleted": "Deleted",
+ "conflictStatusValid": "Compliant",
+ "conflictStatusInvalid": "Non-compliant",
+ "conflictSourceFolderLabel": "Source folder:",
+ "conflictDeleteFileButton": "Delete file",
+ "conflictRestoreFileButton": "Restore",
+ "maxCharsHint": "Maximum {count} characters",
+ "errorMessage": "The number of characters exceeds the allowed limit.",
+ "renameButton": "Confirm new name",
+ "applyRenameButton": "Apply new names",
+ "confirmRenameButton": "Accept names",
+ "renameFilePlaceholder": "Rename the file",
+ "renameConflictsMenuItem": "Rename conflicting files",
+ "noConflictsTitle": "No conflicting files",
+ "noConflictsMessage": "All files match their folder naming convention.",
+ "noDirectRuleOption": "No direct rule for this folder",
+ "inheritedRuleTitle": "An inherited rule applies to this folder",
+ "inheritedRuleDescription": "The rule « {rule} » is set on the folder « {folder} » and also applies to its subfolders. Removing the direct rule for this folder will not disable this inherited rule.",
+ "searchPlaceholder": "Search for a rule",
+ "removeRuleError": "The naming rule could not be removed.",
+ "removeRuleSuccessTitle": "Naming rule removed",
+ "removeRuleSuccessMessage": "The naming rule has been successfully removed.",
+ "modal": {
+ "noStrictTitle": "Rename non-compliant files",
+ "strictTitle": "Rename non-compliant files",
+ "strictDescription": "This folder enforces a strict naming convention. Upload cannot be completed until the files below match the expected rule.",
+ "noStrictDescription": "Build the new name from allowed lists. Free-text fields are validated and preview is generated automatically.",
+ "expectedConventionLabel": "Expected convention in this folder"
+ },
+ "safeZoneModal": {
+ "title": "Warning!",
+ "text": "The naming rule has been modified but not saved. Are you sure you want to close the rule manager? (Changes will be lost)"
+ },
+ "partListInUseModal": {
+ "title": "Unable to delete",
+ "description": "This list cannot be deleted.",
+ "listLabel": "List concerned:",
+ "rulesDescription": "This list is used by the following naming rules:",
+ "resolution": "To delete this list, first update the affected rules so that they no longer use it."
+ }
+ },
"OidcCallbackError": {
"message": "An error occured during authentication...",
"retryButtonText": "Try Again"
@@ -512,6 +631,19 @@
"invitationViewAcceptError": "Unable to accept the invitation",
"folderFetchFolder": "Fail to retrieve folder data",
"bcfDeleteError": "Deletion failed",
+ "namingConstraintsFetchError": "Unable to retrieve naming rules",
+ "namingConstraintFetchError": "Unable to retrieve this naming rule",
+ "namingConstraintCreateError": "Unable to create this naming rule",
+ "namingConstraintUpdateError": "Unable to update this naming rule",
+ "namingConstraintDeleteError": "Unable to delete this naming rule",
+ "namingPartListsFetchError": "Unable to retrieve naming lists",
+ "namingPartListCreateError": "Unable to create this naming list",
+ "namingPartListUpdateError": "Unable to update this naming list",
+ "namingPartListDeleteError": "Unable to delete this naming list",
+ "folderNamingConstraintFetchError": "Unable to retrieve the folder naming rule",
+ "folderNamingConstraintSetError": "Unable to apply the naming rule on this folder",
+ "folderNamingConstraintDeleteError": "Unable to remove the naming rule from this folder",
+ "conflictingDocumentsFetchError": "Unable to retrieve conflicting documents",
"groupImportError": "Group import failure",
"bcfExportXlsxError": "xlsx export failed...",
"invitationViewDeclineError": "Unable to decline invitation"
@@ -966,6 +1098,7 @@
"back": "Back",
"cancel": "Cancel",
"change": "Change",
+ "clear": "Clear",
"comment": "Comment",
"confirm": "Confirm",
"create": "Create",
@@ -984,6 +1117,7 @@
"folder": "Folder",
"import": "Import",
"invalidName": "Invalid name",
+ "invalidNameFormat": "Name does not match the naming rule (e.g. {example}).",
"leave": "Leave",
"modifiedOn": "Last modified",
"modify": "Edit",
@@ -1046,4 +1180,4 @@
"title": "Delete {visasCount} visas",
"message": "You are about to delete the visas on the following files:"
}
-}
\ No newline at end of file
+}
diff --git a/src/i18n/lang/es.json b/src/i18n/lang/es.json
index a17dc7201..2dcff91a5 100644
--- a/src/i18n/lang/es.json
+++ b/src/i18n/lang/es.json
@@ -1,4 +1,35 @@
{
+ "NamingConstraint": {
+ "managerTitle": "Gestión de convenciones de nomenclatura",
+ "constraintsTab": "Reglas de nomenclatura",
+ "strictBadge": "Estricta",
+ "createConstraintTitle": "Creación de una regla",
+ "updateConstraintTitle": "Modificación de una regla",
+ "createListTitle": "Creación de mi lista",
+ "updateListTitle": "Modificación de mi lista",
+ "nameLabel": "Nombre",
+ "strictLabel": "Regla estricta",
+ "strictHelp": "Si marca esta casilla, deberá renombrar todos los archivos que no cumplan esta nueva regla.",
+ "previewLabel": "Vista previa",
+ "ruleSectionTitle": "Regla",
+ "addPartButton": "Añadir un elemento",
+ "emptyParts": "Añada al menos un elemento para definir la regla.",
+ "partTypeLabel": "Tipo",
+ "partTypeValuesIn": "Lista",
+ "partTypeBounded": "Valores acotados",
+ "partTypeNChars": "N caracteres",
+ "elementsHelp": "Lista de valores permitidos separados por comas.",
+ "minLabel": "Mín",
+ "maxLabel": "Máx",
+ "maxLengthLabel": "Longitud máx",
+ "emptyElementsError": "Indique al menos un valor.",
+ "invalidBoundsError": "El máximo debe ser mayor o igual que el mínimo.",
+ "missingBoundsError": "El mínimo y el máximo son obligatorios para los valores acotados.",
+ "invalidMaxLengthError": "La longitud máxima debe ser de al menos 1.",
+ "missingListError": "Seleccione una lista para cada elemento de tipo lista.",
+ "listInUseErrorTitle": "Lista en uso",
+ "listInUseErrorMessage": "Esta lista es utilizada por las siguientes reglas: {constraints}. Modifíquelas antes de eliminar la lista."
+ },
"OidcCallbackError": {
"message": "Ha ocurrido un error de autenticación...",
"retryButtonText": "Intente de nuevo"
@@ -442,6 +473,19 @@
"invitationViewAcceptError": "No se puede aceptar la invitación",
"folderFetchFolder": "Error al recuperar la información de la carpeta",
"bcfDeleteError": "Eliminación fallida...",
+ "namingConstraintsFetchError": "No se pueden recuperar las reglas de nomenclatura",
+ "namingConstraintFetchError": "No se puede recuperar esta regla de nomenclatura",
+ "namingConstraintCreateError": "No se puede crear esta regla de nomenclatura",
+ "namingConstraintUpdateError": "No se puede modificar esta regla de nomenclatura",
+ "namingConstraintDeleteError": "No se puede eliminar esta regla de nomenclatura",
+ "namingPartListsFetchError": "No se pueden recuperar las listas de nomenclatura",
+ "namingPartListCreateError": "No se puede crear esta lista de nomenclatura",
+ "namingPartListUpdateError": "No se puede modificar esta lista de nomenclatura",
+ "namingPartListDeleteError": "No se puede eliminar esta lista de nomenclatura",
+ "folderNamingConstraintFetchError": "No se puede recuperar la regla de nomenclatura de la carpeta",
+ "folderNamingConstraintSetError": "No se puede aplicar la regla de nomenclatura a esta carpeta",
+ "folderNamingConstraintDeleteError": "No se puede quitar la regla de nomenclatura de esta carpeta",
+ "conflictingDocumentsFetchError": "No se pueden recuperar los documentos en conflicto",
"groupImportError": "Error al importar grupos",
"bcfExportXlsxError": "La exportación a Excél ha fracasado",
"invitationViewDeclineError": "No se permite rechazar la invitación"
@@ -973,4 +1017,4 @@
"title": "Eliminación de {visasCount} visas",
"message": "Está a punto de eliminar las visas de los siguientes archivos:"
}
-}
\ No newline at end of file
+}
diff --git a/src/i18n/lang/fr.json b/src/i18n/lang/fr.json
index b4ee5221d..9211b0deb 100644
--- a/src/i18n/lang/fr.json
+++ b/src/i18n/lang/fr.json
@@ -7,6 +7,7 @@
"back": "Retour",
"cancel": "Annuler",
"change": "Changer",
+ "clear": "Effacer",
"comment": "Commenter",
"confirm": "Confirmer",
"create": "Créer",
@@ -31,6 +32,7 @@
"hours_ago": "il y a {count} heure | il y a {count} heures",
"import": "Importer",
"invalidName": "Nom invalide",
+ "invalidNameFormat": "Le nom ne respecte pas la convention de nommage (ex. {example}).",
"just_now": "À l'instant",
"leave": "Quitter",
"location": "Emplacement",
@@ -231,6 +233,130 @@
}
}
},
+ "NamingConstraint": {
+ "managerTitle": "Conventions de nommage",
+ "constraintsTab": "Gestion des règles",
+ "listsTab": "Gestion des listes",
+ "rulesSectionTitle": "Toutes les règles du projet",
+ "addRuleButton": "ajouter une règle",
+ "searchConstraintPlaceholder": "Rechercher une règle",
+ "searchListPlaceholder": "Rechercher une liste",
+ "rulesEmptyTitle": "Pas de règle ajoutée",
+ "rulesEmptyText": "Vous n'avez aucune règle pour l'instant, commencez par en ajouter une.",
+ "rulesEmptyPrerequisitesText": "Les règles de nommage s’appliquent uniquement aux fichiers à l'intérieur d'un dossier.",
+ "manageListsButton": "Gérer les listes",
+ "listsSectionTitle": "Toutes les listes du projet",
+ "createListButton": "Créer une liste",
+ "listsEmptyTitle": "Pas de liste ajoutée",
+ "listsEmptyText": "Vous n'avez aucune liste pour l'instant, commencez par en créer une.",
+ "strictBadge": "Stricte",
+ "createConstraintTitle": "Création d'une règle",
+ "updateConstraintTitle": "Modification d'une règle",
+ "createListTitle": "Création de ma liste",
+ "updateListTitle": "Modification de ma liste",
+ "nameLabel": "Nom",
+ "ruleNameStep": "Nom de la règle",
+ "ruleNamePlaceholder": "Nom de la règle",
+ "separatorStep": "Type de séparateur",
+ "separatorDashOption": "- (tiret)",
+ "separatorDotOption": ". (point)",
+ "separatorUnderscoreOption": "_ (trait de soulignement)",
+ "structureSectionTitle": "Structure de la règle",
+ "strictLabel": "Règle stricte",
+ "strictHelp": "Si vous cochez cette case, vous devrez changer le nom de l'intégralité des fichiers qui ne correspondent pas à cette nouvelle règle.",
+ "previewLabel": "Aperçu :",
+ "ruleSectionTitle": "Règle",
+ "emptyStateHelp": "Ajoutez votre premier élément, pour commencer à établir votre règle.",
+ "addPartButton": "Ajouter un élément :",
+ "emptyParts": "Ajoutez au moins un élément pour définir la règle.",
+ "partTypeLabel": "Type",
+ "partTypeValuesIn": "Liste",
+ "partTypeBounded": "Valeurs bornées",
+ "partTypeNChars": "N caractères",
+ "selectListPlaceholder": "Sélectionner une liste",
+ "noList": "Pas de liste créée",
+ "elementsHelp": "Liste de valeurs autorisées séparées par des virgules.",
+ "minLabel": "Min",
+ "maxLabel": "Max",
+ "maxLengthLabel": "Longueur max",
+ "partNameLabel": "Nom du champ (facultatif)",
+ "partNamePlaceholder": "Saisir un nom",
+ "saveRuleButton": "Enregistrer la règle",
+ "listNameStep": "Nom de la liste",
+ "listNamePlaceholder": "Nom de la liste",
+ "addElementsStep": "Ajouter des éléments",
+ "addOneElementLabel": "Ajouter un élément",
+ "elementNamePlaceholder": "Nom de l'élément",
+ "removeElementButton": "Effacer",
+ "addElementButton": "Ajouter un élément",
+ "bulkImportLabel": "Ajouter plusieurs éléments",
+ "bulkImportPlaceholder": "Ex : PLAN, VISA, DOE",
+ "bulkImportHint": "Entrez vos éléments séparés par des virgules",
+ "importButton": "Générer la liste",
+ "saveListButton": "Enregistrer la liste",
+ "emptyElementsError": "Indiquez au moins une valeur.",
+ "invalidBoundsError": "Le max doit être supérieur ou égal au min.",
+ "missingBoundsError": "Le min et le max sont obligatoires pour les valeurs bornées.",
+ "invalidMaxLengthError": "La longueur max doit être d'au moins 1.",
+ "missingListError": "Sélectionnez une liste pour chaque élément de type liste.",
+ "listInUseErrorTitle": "Liste utilisée",
+ "listInUseErrorMessage": "Cette liste est utilisée par les règles suivantes : {constraints}. Modifiez-les avant de supprimer la liste.",
+ "folderRuleMenuItem": "Convention de nommage",
+ "recursiveLabel": "Inclure les sous-dossiers",
+ "recursiveHelp": "Si vous cochez cette case, cette règle s'appliquera également aux fichiers contenus dans les sous-dossiers de votre dossier principal.",
+ "applyRuleSuccessTitle": "Règle de nommage appliquée",
+ "applyRuleSuccessMessage": "La règle de nommage a bien été appliquée au dossier \"{folder}\".",
+ "applyRuleWithConflictsSuccessTitle": "Règle appliquée avec des conflits",
+ "applyRuleWithConflictsSuccessMessage": "La règle de nommage a bien été appliquée au dossier \"{folder}\". Certains fichiers peuvent encore être en conflit.",
+ "applyRuleError": "La convention de nommage n'a pas pu être appliquée.",
+ "applyRuleWarningTitle": "Le nom ne respecte pas la règle",
+ "conflictTooltip": "Ce fichier ne respecte pas la convention de nommage du dossier.",
+ "conflictModalWarning": "Ce dossier applique une convention de nommage stricte. Les fichiers ci-dessous doivent être renommés ou supprimés avant de pouvoir la sauvegarder.",
+ "conflictCurrentNameLabel": "Nom actuel",
+ "conflictNewNameLabel": "Nouveau nom",
+ "conflictStatusDeleted": "Supprimé",
+ "conflictStatusValid": "Conforme",
+ "conflictStatusInvalid": "Non conforme",
+ "conflictSourceFolderLabel": "Dossier source :",
+ "conflictDeleteFileButton": "Supprimer le fichier",
+ "conflictRestoreFileButton": "Restaurer",
+ "maxCharsHint": "Maximum {count} caractères",
+ "errorMessage": "Le nombre de caractères dépasse la limite autorisée.",
+ "renameButton": "Valider ce nouveau nom",
+ "applyRenameButton": "Confirmer ces nouveaux noms",
+ "confirmRenameButton": "Accepter ces noms",
+ "renameConflictsMenuItem": "Renommer les fichiers en conflit",
+ "noConflictsTitle": "Aucun fichier en conflit",
+ "noConflictsMessage": "Tous les fichiers respectent la convention de nommage de leur dossier.",
+ "noDirectRuleOption": "Aucune règle propre à ce dossier",
+ "inheritedRuleTitle": "Une règle héritée s'applique à ce dossier",
+ "inheritedRuleDescription": "La règle « {rule} » est définie sur le dossier « {folder} » et s'applique également à ses sous-dossiers. Retirer la règle propre à ce dossier ne désactivera pas cette règle héritée.",
+ "searchPlaceholder": "Rechercher une règle",
+ "removeRuleError": "La règle de nommage n'a pas pu être supprimée.",
+ "removeRuleSuccessTitle": "Règle de nommage supprimée",
+ "removeRuleSuccessMessage": "La règle de nommage a été supprimée avec succès.",
+ "modal": {
+ "noStrictTitle": "Renommer les {count} fichiers non conformes",
+ "strictTitle": "Renommer les {count} fichiers non conformes",
+ "strictDescription": "Ce dossier applique une convention de nommage stricte. L'upload ne peut pas être finalisé tant que les fichiers ci-dessous ne respectent pas la règle attendue.",
+ "noStrictDescription": "Construisez le nouveau nom à partir des listes autorisées. Les champs libres sont contrôlés et l'aperçu est généré automatiquement.",
+ "expectedConventionLabel": "Convention attendue dans ce dossier"
+ },
+ "safeZoneModal": {
+ "title": "Attention !",
+ "text": "La règle de nommage a été modifiée mais n'a pas été enregistrée, êtes-vous sûr de vouloir fermer le gestionnaire de règles ? (les modifications seront perdues)"
+ },
+ "partListInUseModal": {
+ "title": "Suppression impossible",
+ "description": "Cette liste ne peut pas être supprimée.",
+ "listLabel": "Liste concernée :",
+ "rulesDescription": "Cette liste est utilisée par les règles de nommage suivantes :",
+ "resolution": "Pour supprimer cette liste, modifiez d'abord les règles concernées afin qu'elles ne l'utilisent plus."
+ }
+ },
+ "FolderNamingConstraint": {
+ "ruleSectionTitle": "Règles appliquées au dossier : "
+ },
"LanguageSelector": {
"title": "Choix de la langue"
},
@@ -614,6 +740,19 @@
"bcfExportError": "L'export BCF échoué...",
"bcfExportXlsxError": "L'export Excel a échoué...",
"bcfDeleteError": "La suppression a échouée...",
+ "namingConstraintsFetchError": "Impossible de récupérer les règles de nommage",
+ "namingConstraintFetchError": "Impossible de récupérer cette règle de nommage",
+ "namingConstraintCreateError": "Impossible de créer cette règle de nommage",
+ "namingConstraintUpdateError": "Impossible de modifier cette règle de nommage",
+ "namingConstraintDeleteError": "Impossible de supprimer cette règle de nommage",
+ "namingPartListsFetchError": "Impossible de récupérer les listes de nommage",
+ "namingPartListCreateError": "Impossible de créer cette liste de nommage",
+ "namingPartListUpdateError": "Impossible de modifier cette liste de nommage",
+ "namingPartListDeleteError": "Impossible de supprimer cette liste de nommage",
+ "folderNamingConstraintFetchError": "Impossible de récupérer la règle de nommage du dossier",
+ "folderNamingConstraintSetError": "Impossible d'appliquer la règle de nommage sur ce dossier",
+ "folderNamingConstraintDeleteError": "Impossible de retirer la règle de nommage de ce dossier",
+ "conflictingDocumentsFetchError": "Impossible de récupérer les documents en conflit",
"fileVersionsFetchError": "Impossible de récupérer les versions de ce document",
"fileVersionsMakeHeadError": "Échec lors du passage du document en version actuelle",
"fileVersionDelete": "Échec lors de la suppression du document",
@@ -1071,4 +1210,4 @@
"title": "Suppression de {visasCount} visas",
"message": "Vous êtes sur le point de supprimer les visas sur les fichiers suivants :"
}
-}
\ No newline at end of file
+}
diff --git a/src/i18n/lang/it.json b/src/i18n/lang/it.json
index ac654b924..16e521a0e 100644
--- a/src/i18n/lang/it.json
+++ b/src/i18n/lang/it.json
@@ -1,4 +1,35 @@
{
+ "NamingConstraint": {
+ "managerTitle": "Gestione delle convenzioni di denominazione",
+ "constraintsTab": "Regole di denominazione",
+ "strictBadge": "Rigida",
+ "createConstraintTitle": "Creazione di una regola",
+ "updateConstraintTitle": "Modifica di una regola",
+ "createListTitle": "Creazione della mia lista",
+ "updateListTitle": "Modifica della mia lista",
+ "nameLabel": "Nome",
+ "strictLabel": "Regola rigida",
+ "strictHelp": "Se selezioni questa casella, dovrai rinominare tutti i file che non rispettano questa nuova regola.",
+ "previewLabel": "Anteprima",
+ "ruleSectionTitle": "Regola",
+ "addPartButton": "Aggiungi un elemento",
+ "emptyParts": "Aggiungi almeno un elemento per definire la regola.",
+ "partTypeLabel": "Tipo",
+ "partTypeValuesIn": "Lista",
+ "partTypeBounded": "Valori delimitati",
+ "partTypeNChars": "N caratteri",
+ "elementsHelp": "Elenco di valori consentiti separati da virgole.",
+ "minLabel": "Min",
+ "maxLabel": "Max",
+ "maxLengthLabel": "Lunghezza max",
+ "emptyElementsError": "Indica almeno un valore.",
+ "invalidBoundsError": "Il massimo deve essere maggiore o uguale al minimo.",
+ "missingBoundsError": "Il minimo e il massimo sono obbligatori per i valori delimitati.",
+ "invalidMaxLengthError": "La lunghezza massima deve essere almeno 1.",
+ "missingListError": "Seleziona una lista per ogni elemento di tipo lista.",
+ "listInUseErrorTitle": "Lista in uso",
+ "listInUseErrorMessage": "Questa lista è utilizzata dalle seguenti regole: {constraints}. Modificale prima di eliminare la lista."
+ },
"OidcCallbackError": {
"message": "Si è verificato un errore durante l'autenticazione...",
"retryButtonText": "Riprova"
@@ -363,6 +394,19 @@
"invitationViewDeclineError": "Impossibile rifiutare l'invito",
"folderFetchFolder": "Errore durante il recupero delle informazioni sulla cartella",
"bcfDeleteError": "Eliminazione non riuscita...",
+ "namingConstraintsFetchError": "Impossibile recuperare le regole di denominazione",
+ "namingConstraintFetchError": "Impossibile recuperare questa regola di denominazione",
+ "namingConstraintCreateError": "Impossibile creare questa regola di denominazione",
+ "namingConstraintUpdateError": "Impossibile aggiornare questa regola di denominazione",
+ "namingConstraintDeleteError": "Impossibile eliminare questa regola di denominazione",
+ "namingPartListsFetchError": "Impossibile recuperare le liste di denominazione",
+ "namingPartListCreateError": "Impossibile creare questa lista di denominazione",
+ "namingPartListUpdateError": "Impossibile aggiornare questa lista di denominazione",
+ "namingPartListDeleteError": "Impossibile eliminare questa lista di denominazione",
+ "folderNamingConstraintFetchError": "Impossibile recuperare la regola di denominazione della cartella",
+ "folderNamingConstraintSetError": "Impossibile applicare la regola di denominazione a questa cartella",
+ "folderNamingConstraintDeleteError": "Impossibile rimuovere la regola di denominazione da questa cartella",
+ "conflictingDocumentsFetchError": "Impossibile recuperare i documenti in conflitto",
"groupImportError": "Impossibile importare i gruppi"
},
"ProjectStatusBadge": {
@@ -857,4 +901,4 @@
"FileTreePreviewModal": {
"title": "Importa la struttura del file da"
}
-}
\ No newline at end of file
+}
diff --git a/src/i18n/lang/nl.json b/src/i18n/lang/nl.json
index 8680aa46c..1839f3586 100644
--- a/src/i18n/lang/nl.json
+++ b/src/i18n/lang/nl.json
@@ -1,7 +1,38 @@
{
+ "NamingConstraint": {
+ "managerTitle": "Beheer van naamgevingsconventies",
+ "constraintsTab": "Naamgevingsregels",
+ "strictBadge": "Strikt",
+ "createConstraintTitle": "Regel aanmaken",
+ "updateConstraintTitle": "Regel bewerken",
+ "createListTitle": "Mijn lijst aanmaken",
+ "updateListTitle": "Mijn lijst bewerken",
+ "nameLabel": "Naam",
+ "strictLabel": "Strikte regel",
+ "strictHelp": "Als u dit vakje aanvinkt, moet u alle bestanden hernoemen die niet aan deze nieuwe regel voldoen.",
+ "previewLabel": "Voorbeeld",
+ "ruleSectionTitle": "Regel",
+ "addPartButton": "Element toevoegen",
+ "emptyParts": "Voeg ten minste één element toe om de regel te definiëren.",
+ "partTypeLabel": "Type",
+ "partTypeValuesIn": "Lijst",
+ "partTypeBounded": "Begrensde waarden",
+ "partTypeNChars": "N tekens",
+ "elementsHelp": "Door komma's gescheiden lijst van toegestane waarden.",
+ "minLabel": "Min",
+ "maxLabel": "Max",
+ "maxLengthLabel": "Max. lengte",
+ "emptyElementsError": "Geef ten minste één waarde op.",
+ "invalidBoundsError": "Max moet groter dan of gelijk aan min zijn.",
+ "missingBoundsError": "Min en max zijn verplicht voor begrensde waarden.",
+ "invalidMaxLengthError": "De maximale lengte moet ten minste 1 zijn.",
+ "missingListError": "Selecteer een lijst voor elk lijstelement.",
+ "listInUseErrorTitle": "Lijst in gebruik",
+ "listInUseErrorMessage": "Deze lijst wordt gebruikt door de volgende regels: {constraints}. Pas ze aan voordat u de lijst verwijdert."
+ },
"OidcCallbackError": {
"message": "Er heeft zich een fout voorgedaan bij de authenticatie",
- "retryButtonText": ""
+ "retryButtonText": "Opnieuw proberen"
},
"ProjectBoard": {
"tabs": {
@@ -19,7 +50,7 @@
},
"ProjectOverview": {
"openFileUploadButtonText": "Voeg een model toe",
- "forbiddenUploadNotification": ""
+ "forbiddenUploadNotification": "Selecteer een modelbestand: {extensions}"
},
"LanguageSelector": {
"title": "Kies een taal"
@@ -54,8 +85,8 @@
"manageAccessButtonText": "Toegang beheren",
"createModelButtonText": "Aanmerken als model",
"removeModelButtonText": "Modellen verwijderen",
- "versioningButtonText": "",
- "visaButtonText": ""
+ "versioningButtonText": "Een versie toevoegen",
+ "visaButtonText": "Validatieverzoek"
},
"FilesActionBar": {
"moveButtonText": "Verplaatsen naar"
@@ -68,16 +99,16 @@
"FilesManager": {
"title": "Projectbestanden",
"createModelNotification": "Model succesvol aangemaakt",
- "structureImport": "",
- "gedDownload": "",
- "folderImport": ""
+ "structureImport": "Een DMS-structuur importeren",
+ "gedDownload": "Het DMS downloaden",
+ "folderImport": "Een map importeren"
},
"FilesManagerOnboarding": {
"text": "Upload uw eerste bestand",
"uploadFileButtonText": "Upload een bestand",
"createFolderButtonText": "Maak een map aan",
- "GEDStructureImport": "",
- "folderImport": ""
+ "GEDStructureImport": "Een DMS-structuur importeren",
+ "folderImport": "Een bestand importeren"
},
"FilesTable": {
"headers": {
@@ -96,11 +127,11 @@
"rightsHeader": "Rechten",
"membersHeader": "Leden",
"groupHeader": "Groep",
- "propagate": ""
+ "propagate": "Vink het vakje aan voordat u het formulier valideert om de rechten (recursief) door te geven aan submappen."
},
"FolderSelector": {
"selectedFolder": "Geselecteerde map",
- "moveHere": ""
+ "moveHere": "Hierheen verplaatsen"
},
"GroupCardActionMenu": {
"changeColorButtonText": "Kleur veranderen"
@@ -187,8 +218,8 @@
"version": "Versie"
},
"emptyTablePlaceholder": {
- "text": "",
- "textPdf": ""
+ "text": "Geen model",
+ "textPdf": "Klik in het menu \"bestand\" van de bestandsbeheerder op \"Als model instellen\" om uw pdf-plannen beschikbaar te maken in de modellenbeheerder."
}
},
"ProjectCardDeleteGuard": {
@@ -343,23 +374,36 @@
"fileVersionsMakeHeadError": "Naar huidige versie gaan niet gelukt.",
"fileVersionDelete": "Document verwijderen mislukt",
"userAlreadyExistsError": "Deze gebruiker is al een lid van de ruimte/het project.",
- "tagsFetchError": "",
- "tagsCreateError": "",
- "tagsUpdateError": "",
- "tagsDeleteError": "",
- "tagsAddDocumentError": "",
- "tagsDeleteDocumentError": "",
- "spaceSizeFullError": "",
- "spaceFetchError": "",
- "projectFolderTreeFetchError": "",
- "fileStructureCreateError": "",
- "documentCreateError": "",
+ "tagsFetchError": "Kan labels niet ophalen",
+ "tagsCreateError": "Fout bij het aanmaken van het label",
+ "tagsUpdateError": "Fout bij het wijzigen van het label",
+ "tagsDeleteError": "Fout bij het verwijderen van het label",
+ "tagsAddDocumentError": "Fout bij het koppelen van het label aan het document",
+ "tagsDeleteDocumentError": "Fout bij het ontkoppelen van het label van het document",
+ "spaceSizeFullError": "Uw opslagruimte is vol",
+ "spaceFetchError": "Kan de gegevens van de ruimte niet ophalen",
+ "projectFolderTreeFetchError": "Kan de projectenlijst niet ophalen",
+ "fileStructureCreateError": "Kan de bestandsstructuur niet aanmaken",
+ "documentCreateError": "Kan dit document niet uploaden",
"invitationViewFetchError": "Ophalen van uitnodigingen mislukt",
- "invitationViewAcceptError": "",
- "invitationViewDeclineError": "",
- "folderFetchFolder": "",
- "bcfDeleteError": "",
- "groupImportError": ""
+ "invitationViewAcceptError": "Kan de uitnodiging niet accepteren",
+ "invitationViewDeclineError": "Kan de uitnodiging niet weigeren",
+ "folderFetchFolder": "Kan de mapgegevens niet ophalen",
+ "bcfDeleteError": "Verwijderen mislukt",
+ "namingConstraintsFetchError": "Kan naamgevingsregels niet ophalen",
+ "namingConstraintFetchError": "Kan deze naamgevingsregel niet ophalen",
+ "namingConstraintCreateError": "Kan deze naamgevingsregel niet aanmaken",
+ "namingConstraintUpdateError": "Kan deze naamgevingsregel niet bijwerken",
+ "namingConstraintDeleteError": "Kan deze naamgevingsregel niet verwijderen",
+ "namingPartListsFetchError": "Kan naamgevingslijsten niet ophalen",
+ "namingPartListCreateError": "Kan deze naamgevingslijst niet aanmaken",
+ "namingPartListUpdateError": "Kan deze naamgevingslijst niet bijwerken",
+ "namingPartListDeleteError": "Kan deze naamgevingslijst niet verwijderen",
+ "folderNamingConstraintFetchError": "Kan de naamgevingsregel van de map niet ophalen",
+ "folderNamingConstraintSetError": "Kan de naamgevingsregel niet op deze map toepassen",
+ "folderNamingConstraintDeleteError": "Kan de naamgevingsregel niet van deze map verwijderen",
+ "conflictingDocumentsFetchError": "Kan conflicterende documenten niet ophalen",
+ "groupImportError": "Importeren van groep mislukt"
},
"ProjectStatusBadge": {
"new": "NIEUW",
@@ -424,7 +468,7 @@
"deleted": "Verwijderd",
"paused": "Gepauzeerd",
"custom": "Beheerd door BIMData.io",
- "past_due": ""
+ "past_due": "Opgeschort"
}
},
"DatapackForm": {
@@ -603,31 +647,31 @@
"comments": {
"commentary": "Toelichting",
"publish": "Publiceren",
- "commentInputPlaceholder": "",
- "confirmDelete": ""
+ "commentInputPlaceholder": "Een opmerking plaatsen",
+ "confirmDelete": "Deze opmerking verwijderen?"
},
"selectionValidator": {
"warning": "Deze gebruiker heeft geen toegang tot het bestand."
},
"noVisa": "Er is nog geen VISA aangemaakt",
- "accept": "",
- "close": "",
+ "accept": "Accepteren",
+ "close": "Sluiten",
"deny": "Weigeren",
"term": "Vervaldatum",
- "validator": "",
- "validators": "",
+ "validator": "Validator",
+ "validators": "Validators",
"safeZone": {
"add": {
- "text": ""
+ "text": "Weet u zeker dat u deze pagina wilt verlaten? Er kan niet-gecontroleerde informatie zijn."
},
"delete": {
- "text": ""
+ "text": "Weet u zeker dat u deze documentgoedkeuring wilt verwijderen?"
},
"close": {
- "text": ""
+ "text": "Weet u zeker dat u deze documentgoedkeuring wilt sluiten?"
},
"deleteComment": {
- "text": ""
+ "text": "Weet u zeker dat u deze opmerking wilt verwijderen?"
}
}
},
@@ -649,17 +693,17 @@
"metricsEmptyText": "Voeg problemen toe om hier de bijbehorende statistieken te bekijken.",
"noSearchResultsTitle": "Geen resultaten",
"noSearchResultsText": "Je zoekopdracht heeft geen resultaten opgeleverd, bekijk nog een keer je filters en zoekwoorden.",
- "importBcfNotificationSuccess": "",
- "importBcfNotificationError": "",
- "exportBcfNotificationSuccess": "",
- "exportBcfNotificationError": "",
- "selectedTopicCount": "",
- "deleteBcfText": "",
- "deleteInProgress": "",
+ "importBcfNotificationSuccess": "BCF-bestand succesvol geïmporteerd",
+ "importBcfNotificationError": "Fout bij het importeren van het BCF-bestand",
+ "exportBcfNotificationSuccess": "BCF succesvol geëxporteerd",
+ "exportBcfNotificationError": "Fout bij het exporteren van de BCF naar Xlsx",
+ "selectedTopicCount": "Aantal geselecteerde topics",
+ "deleteBcfText": "U staat op het punt alle geselecteerde topics te verwijderen",
+ "deleteInProgress": "Bezig met verwijderen",
"deleteInProgressSub": "Dit kan even duren.",
- "deleteBcfNotificationSuccess": "",
- "deleteBcfNotificationError": "",
- "onlyAdminParameters": ""
+ "deleteBcfNotificationSuccess": "Topics verwijderd",
+ "deleteBcfNotificationError": "Fout bij het verwijderen van topics",
+ "onlyAdminParameters": "U moet projectbeheerder zijn"
},
"GuidedTour": {
"platform": {
@@ -723,7 +767,7 @@
"AddVersion": "Voeg een nieuwe versie toe",
"loading": {
"subMessage": "Dit kan even duren.",
- "downloadMessage": "",
+ "downloadMessage": "Bezig met laden.",
"deleteMessage": "Verwijdering bezig."
},
"getToHead": "Ga naar de huidige versie",
@@ -737,34 +781,34 @@
"emptyArea": "Je hebt momenteel geen versiegeschiedenis voor dit bestand."
},
"Tag": {
- "title": "",
- "list": "",
- "addTag": "",
- "addNewTag": "",
- "deleteTag": ""
+ "title": "Labelbeheer",
+ "list": "Lijst met labels",
+ "addTag": "een label toevoegen",
+ "addNewTag": "Een nieuw label toevoegen",
+ "deleteTag": "Dit label verwijderen"
},
"SubscriptionModal": {
- "title": "",
- "subtitle": "",
+ "title": "Meer ruimte krijgen",
+ "subtitle": "U beschikt momenteel over {size} opslagcapaciteit",
"soon": "SOON",
"upgrade": {
- "title": "",
- "subtitle": ""
+ "title": "Upgraden naar het pro-platform",
+ "subtitle": "Abonneer u op BIMData Pro om meer ruimte te krijgen."
},
"referal": {
- "title": "",
- "subtitle": ""
+ "title": "Doe mee met ons verwijzingsprogramma!",
+ "subtitle": "Deel uw verwijzingscode om ruimte te verdienen."
},
"linkedin": {
- "title": "",
- "subtitle": ""
+ "title": "Koppel uw LinkedIn-account",
+ "subtitle": "Volg de BIMData-pagina om ruimte te verdienen."
}
},
"GroupMemberCard": {
- "pending": ""
+ "pending": "Uitnodiging in behandeling"
},
"UserProfile": {
- "title": "",
+ "title": "Profielinstellingen",
"loading": {
"message": "Verwijdering bezig.",
"subMessage": "Dit kan even duren."
@@ -772,32 +816,32 @@
},
"Invitations": {
"title": "Mijn uitnodigingen",
- "invitCounter": "",
- "acceptAll": "",
- "invitedInProject": "",
- "invitedInSpace": "",
- "invitAccepted": "",
- "invitDenied": "",
- "invitPending": "",
+ "invitCounter": "Openstaande uitnodigingen -",
+ "acceptAll": "Alles accepteren",
+ "invitedInProject": "U bent door {sender} uitgenodigd om deel te nemen aan het project {project} in de ruimte {cloud}",
+ "invitedInSpace": "U bent door {sender} uitgenodigd om deel te nemen aan de ruimte {cloud}",
+ "invitAccepted": "Uitnodiging geaccepteerd",
+ "invitDenied": "Uitnodiging geweigerd",
+ "invitPending": "Uitnodiging in behandeling",
"goToProjectLong": "Project",
"goToProjectShort": "Project",
"goToSpaceLong": "Ruimte",
"goToSpaceShort": "Ruimte"
},
"DocumentViewer": {
- "viewerButton": "",
- "docPlaceholder": ""
+ "viewerButton": "Openen in de viewer",
+ "docPlaceholder": "Geen voorbeeld voor dit document"
},
"GroupImport": {
- "title": "",
- "selectAll": "",
- "notAdmin": "",
- "noGroup": ""
+ "title": "Een groep importeren",
+ "selectAll": "Alles selecteren",
+ "notAdmin": "U moet projectbeheerder zijn",
+ "noGroup": "Er is geen groep voor dit project"
},
"UploadCard": {
"cancelMessage": "Geannuleerd",
"errorMessage": "Upload mislukt",
- "upload": ""
+ "upload": "{uploaded} van {total} ({percentage}% geüpload)"
},
"t": {
"add": "Toevoegen",
@@ -818,39 +862,39 @@
"edit": "Bewerken",
"emptyFolder": "Lege map",
"error": "Fout",
- "export": "",
- "file": "",
- "folder": "",
- "import": "",
- "invalidName": "",
- "leave": "",
- "modifiedOn": "",
- "modify": "",
- "name": "",
- "ok": "",
- "open": "",
- "or": "",
- "rename": "",
- "search": "",
- "size": "",
- "space": "",
- "status": "",
- "success": "",
- "type": "",
- "unarchive": "",
- "validate": "",
- "verify": ""
+ "export": "Exporteren",
+ "file": "Bestand",
+ "folder": "Map",
+ "import": "Importeren",
+ "invalidName": "Ongeldige naam",
+ "leave": "Verlaten",
+ "modifiedOn": "Laatst gewijzigd",
+ "modify": "Bewerken",
+ "name": "Naam",
+ "ok": "OK",
+ "open": "Openen",
+ "or": "of",
+ "rename": "Hernoemen",
+ "search": "Zoeken",
+ "size": "Grootte",
+ "space": "Ruimte",
+ "status": "Status",
+ "success": "Gelukt",
+ "type": "Type",
+ "unarchive": "Dearchiveren",
+ "validate": "Valideren",
+ "verify": "Controleren"
},
"FolderCreationButton": {
- "text": ""
+ "text": "Nieuwe map"
},
"FolderCreationForm": {
- "title": ""
+ "title": "Map aanmaken"
},
"SpaceCardImageButton": {
- "text": ""
+ "text": "Afbeelding wijzigen"
},
"FileTreePreviewModal": {
- "title": ""
+ "title": "DMS-structuur importeren uit"
}
-}
\ No newline at end of file
+}
diff --git a/src/i18n/lang/no.json b/src/i18n/lang/no.json
index 610a8e9d8..64963f94a 100644
--- a/src/i18n/lang/no.json
+++ b/src/i18n/lang/no.json
@@ -1,7 +1,38 @@
{
+ "NamingConstraint": {
+ "managerTitle": "Administrasjon av navnekonvensjoner",
+ "constraintsTab": "Navneregler",
+ "strictBadge": "Streng",
+ "createConstraintTitle": "Opprette en regel",
+ "updateConstraintTitle": "Redigere en regel",
+ "createListTitle": "Opprette listen min",
+ "updateListTitle": "Redigere listen min",
+ "nameLabel": "Navn",
+ "strictLabel": "Streng regel",
+ "strictHelp": "Hvis du krysser av i denne boksen, må du gi nytt navn til alle filer som ikke samsvarer med denne nye regelen.",
+ "previewLabel": "Forhåndsvisning",
+ "ruleSectionTitle": "Regel",
+ "addPartButton": "Legg til et element",
+ "emptyParts": "Legg til minst ett element for å definere regelen.",
+ "partTypeLabel": "Type",
+ "partTypeValuesIn": "Liste",
+ "partTypeBounded": "Avgrensede verdier",
+ "partTypeNChars": "N tegn",
+ "elementsHelp": "Kommaseparert liste over tillatte verdier.",
+ "minLabel": "Min",
+ "maxLabel": "Maks",
+ "maxLengthLabel": "Maks lengde",
+ "emptyElementsError": "Oppgi minst én verdi.",
+ "invalidBoundsError": "Maks må være større enn eller lik min.",
+ "missingBoundsError": "Min og maks er påkrevd for avgrensede verdier.",
+ "invalidMaxLengthError": "Maks lengde må være minst 1.",
+ "missingListError": "Velg en liste for hvert listeelement.",
+ "listInUseErrorTitle": "Liste i bruk",
+ "listInUseErrorMessage": "Denne listen brukes av følgende regler: {constraints}. Rediger dem før du sletter listen."
+ },
"OidcCallbackError": {
"message": "En feil oppsto under autentiseringen...",
- "retryButtonText": ""
+ "retryButtonText": "Prøv igjen"
},
"ProjectBoard": {
"tabs": {
@@ -19,7 +50,7 @@
},
"ProjectOverview": {
"openFileUploadButtonText": "Legg til en modell",
- "forbiddenUploadNotification": ""
+ "forbiddenUploadNotification": "Velg en modellfil: {extensions}"
},
"LanguageSelector": {
"title": "Velg et språk"
@@ -54,8 +85,8 @@
"manageAccessButtonText": "Administrer tilgang",
"createModelButtonText": "Definer som modell",
"removeModelButtonText": "Fjern modeller",
- "versioningButtonText": "",
- "visaButtonText": ""
+ "versioningButtonText": "Legg til en versjon",
+ "visaButtonText": "Valideringsforespørsel"
},
"FilesActionBar": {
"moveButtonText": "Flytt til"
@@ -68,16 +99,16 @@
"FilesManager": {
"title": "Prosjektfiler",
"createModelNotification": "Modell opprettet",
- "structureImport": "",
- "gedDownload": "",
- "folderImport": ""
+ "structureImport": "Importer en DMS-struktur",
+ "gedDownload": "Last ned DMS",
+ "folderImport": "Importer en mappe"
},
"FilesManagerOnboarding": {
"text": "Last opp din første fil",
"uploadFileButtonText": "Last opp en fil",
"createFolderButtonText": "Opprett en mappe",
- "GEDStructureImport": "",
- "folderImport": ""
+ "GEDStructureImport": "Importer en DMS-struktur",
+ "folderImport": "Importer en fil"
},
"FilesTable": {
"headers": {
@@ -96,11 +127,11 @@
"rightsHeader": "Rettigheter",
"membersHeader": "Medlemmer",
"groupHeader": "Gruppe",
- "propagate": ""
+ "propagate": "For å overføre rettighetene til undermapper (rekursivt), kryss av i boksen før du validerer skjemaet."
},
"FolderSelector": {
"selectedFolder": "Valgt mappe",
- "moveHere": ""
+ "moveHere": "Flytt hit"
},
"GroupCardActionMenu": {
"changeColorButtonText": "Endre farge"
@@ -187,8 +218,8 @@
"version": "Versjon"
},
"emptyTablePlaceholder": {
- "text": "",
- "textPdf": ""
+ "text": "Ingen modell",
+ "textPdf": "Fra «fil»-menyen i filbehandleren klikker du på «Definer som modell» for å gjøre pdf-tegningene dine tilgjengelige i modellbehandleren."
}
},
"ProjectCardDeleteGuard": {
@@ -343,23 +374,36 @@
"fileVersionsMakeHeadError": "Bytte av dokument til nåværende versjon feilet.",
"fileVersionDelete": "Sletting av dokumentet feilet",
"userAlreadyExistsError": "Brukeren er allerede medlem av dette området/prosjektet.",
- "tagsFetchError": "",
- "tagsCreateError": "",
- "tagsUpdateError": "",
- "tagsDeleteError": "",
- "tagsAddDocumentError": "",
- "tagsDeleteDocumentError": "",
- "spaceSizeFullError": "",
- "spaceFetchError": "",
- "projectFolderTreeFetchError": "",
- "fileStructureCreateError": "",
- "documentCreateError": "",
+ "tagsFetchError": "Kunne ikke hente tagger",
+ "tagsCreateError": "Feil under opprettelse av taggen",
+ "tagsUpdateError": "Feil under endring av taggen",
+ "tagsDeleteError": "Feil under sletting av taggen",
+ "tagsAddDocumentError": "Feil under tilknytning av taggen til dokumentet",
+ "tagsDeleteDocumentError": "Feil under fjerning av taggen fra dokumentet",
+ "spaceSizeFullError": "Lagringsplassen din er full",
+ "spaceFetchError": "Kunne ikke hente informasjon om området",
+ "projectFolderTreeFetchError": "Kunne ikke hente prosjektlisten",
+ "fileStructureCreateError": "Kunne ikke opprette filstrukturen",
+ "documentCreateError": "Kunne ikke laste opp dette dokumentet",
"invitationViewFetchError": "Kunne ikke hente frem invitasjoner",
- "invitationViewAcceptError": "",
- "invitationViewDeclineError": "",
- "folderFetchFolder": "",
- "bcfDeleteError": "",
- "groupImportError": ""
+ "invitationViewAcceptError": "Kunne ikke godta invitasjonen",
+ "invitationViewDeclineError": "Kunne ikke avslå invitasjonen",
+ "folderFetchFolder": "Kunne ikke hente mappedata",
+ "bcfDeleteError": "Sletting mislyktes",
+ "namingConstraintsFetchError": "Kunne ikke hente navneregler",
+ "namingConstraintFetchError": "Kunne ikke hente denne navneregelen",
+ "namingConstraintCreateError": "Kunne ikke opprette denne navneregelen",
+ "namingConstraintUpdateError": "Kunne ikke oppdatere denne navneregelen",
+ "namingConstraintDeleteError": "Kunne ikke slette denne navneregelen",
+ "namingPartListsFetchError": "Kunne ikke hente navnelister",
+ "namingPartListCreateError": "Kunne ikke opprette denne navnelisten",
+ "namingPartListUpdateError": "Kunne ikke oppdatere denne navnelisten",
+ "namingPartListDeleteError": "Kunne ikke slette denne navnelisten",
+ "folderNamingConstraintFetchError": "Kunne ikke hente navneregelen for mappen",
+ "folderNamingConstraintSetError": "Kunne ikke bruke navneregelen på denne mappen",
+ "folderNamingConstraintDeleteError": "Kunne ikke fjerne navneregelen fra denne mappen",
+ "conflictingDocumentsFetchError": "Kunne ikke hente dokumenter i konflikt",
+ "groupImportError": "Import av gruppe mislyktes"
},
"ProjectStatusBadge": {
"new": "NY",
@@ -424,7 +468,7 @@
"deleted": "Slettet",
"paused": "Satt på pause",
"custom": "Administrert av BIMData.io",
- "past_due": ""
+ "past_due": "Suspendert"
}
},
"DatapackForm": {
@@ -649,17 +693,17 @@
"metricsEmptyText": "Legg til problemer for å se assosiert statistikk her.",
"noSearchResultsTitle": "Ingen resultater",
"noSearchResultsText": "Søket ditt ga ingen resultater, vennligst sjekk filter eller nøkkelord",
- "importBcfNotificationSuccess": "",
- "importBcfNotificationError": "",
- "exportBcfNotificationSuccess": "",
- "exportBcfNotificationError": "",
- "selectedTopicCount": "",
- "deleteBcfText": "",
- "deleteInProgress": "",
+ "importBcfNotificationSuccess": "BCF-filen ble importert",
+ "importBcfNotificationError": "Feil under import av BCF-filen",
+ "exportBcfNotificationSuccess": "BCF ble eksportert",
+ "exportBcfNotificationError": "Feil under eksport av BCF til Xlsx",
+ "selectedTopicCount": "Antall valgte emner",
+ "deleteBcfText": "Du er i ferd med å slette alle valgte emner",
+ "deleteInProgress": "Sletting pågår",
"deleteInProgressSub": "Denne prosessen kan ta en liten stund.",
- "deleteBcfNotificationSuccess": "",
- "deleteBcfNotificationError": "",
- "onlyAdminParameters": ""
+ "deleteBcfNotificationSuccess": "Emner slettet",
+ "deleteBcfNotificationError": "Feil under sletting av emner",
+ "onlyAdminParameters": "Du må være prosjektadministrator"
},
"GuidedTour": {
"platform": {
@@ -723,8 +767,8 @@
"AddVersion": "Legg til ny versjon",
"loading": {
"subMessage": "Denne prosessen kan ta en liten stund.",
- "downloadMessage": "",
- "deleteMessage": ""
+ "downloadMessage": "Lasting pågår.",
+ "deleteMessage": "Sletting pågår."
},
"getToHead": "Bytt til nåværende versjon",
"show": "Se",
@@ -737,67 +781,67 @@
"emptyArea": "Du har for øyeblikket ingen versjonshistorikk for denne filen."
},
"Tag": {
- "title": "",
- "list": "",
- "addTag": "",
- "addNewTag": "",
- "deleteTag": ""
+ "title": "Administrasjon av tagger",
+ "list": "Liste over tagger",
+ "addTag": "legg til en tagg",
+ "addNewTag": "Legg til en ny tagg",
+ "deleteTag": "Slett denne taggen"
},
"SubscriptionModal": {
- "title": "",
- "subtitle": "",
+ "title": "Få mer plass",
+ "subtitle": "Du har for øyeblikket {size} lagringskapasitet",
"soon": "SOON",
"upgrade": {
- "title": "",
- "subtitle": ""
+ "title": "Oppgrader til pro-plattformen",
+ "subtitle": "Abonner på BIMData Pro for å få mer plass."
},
"referal": {
- "title": "",
- "subtitle": ""
+ "title": "Bli med i vervingsprogrammet vårt!",
+ "subtitle": "Del vervekoden din for å tjene plass."
},
"linkedin": {
- "title": "",
- "subtitle": ""
+ "title": "Koble til LinkedIn-kontoen din",
+ "subtitle": "Følg BIMData-siden for å tjene plass."
}
},
"GroupMemberCard": {
- "pending": ""
+ "pending": "Invitasjon venter"
},
"UserProfile": {
- "title": "",
+ "title": "Profilinnstillinger",
"loading": {
- "message": "",
- "subMessage": ""
+ "message": "Sletting pågår.",
+ "subMessage": "Denne prosessen kan ta litt tid, ikke forlat siden."
}
},
"Invitations": {
- "title": "",
- "invitCounter": "",
- "acceptAll": "",
- "invitedInProject": "",
- "invitedInSpace": "",
- "invitAccepted": "",
- "invitDenied": "",
- "invitPending": "",
+ "title": "Mine invitasjoner",
+ "invitCounter": "Ventende invitasjoner -",
+ "acceptAll": "Godta alle",
+ "invitedInProject": "Du er invitert av {sender} til å bli med i prosjektet {project} i området {cloud}",
+ "invitedInSpace": "Du er invitert av {sender} til å bli med i området {cloud}",
+ "invitAccepted": "Invitasjon godtatt",
+ "invitDenied": "Invitasjon avslått",
+ "invitPending": "Invitasjon venter",
"goToProjectLong": "Prosjekt",
"goToProjectShort": "Prosjekt",
"goToSpaceLong": "Område",
"goToSpaceShort": "Område"
},
"DocumentViewer": {
- "viewerButton": "",
- "docPlaceholder": ""
+ "viewerButton": "Åpne i visningsprogrammet",
+ "docPlaceholder": "Ingen forhåndsvisning for dette dokumentet"
},
"GroupImport": {
- "title": "",
+ "title": "Importer en gruppe",
"selectAll": "Velg alle",
- "notAdmin": "",
- "noGroup": ""
+ "notAdmin": "Du må være prosjektadministrator",
+ "noGroup": "Det finnes ingen gruppe for dette prosjektet"
},
"UploadCard": {
"cancelMessage": "Avbrutt",
"errorMessage": "Opplasting feilet",
- "upload": ""
+ "upload": "{uploaded} av {total} ({percentage}% lastet opp)"
},
"t": {
"add": "Legg til",
@@ -818,39 +862,39 @@
"edit": "Rediger",
"emptyFolder": "Tom mappe",
"error": "Feil",
- "export": "",
- "file": "",
- "folder": "",
- "import": "",
- "invalidName": "",
- "leave": "",
- "modifiedOn": "",
- "modify": "",
- "name": "",
- "ok": "",
- "open": "",
- "or": "",
- "rename": "",
- "search": "",
- "size": "",
- "space": "",
- "status": "",
- "success": "",
- "type": "",
- "unarchive": "",
- "validate": "",
- "verify": ""
+ "export": "Eksporter",
+ "file": "Fil",
+ "folder": "Mappe",
+ "import": "Importer",
+ "invalidName": "Ugyldig navn",
+ "leave": "Forlat",
+ "modifiedOn": "Sist endret",
+ "modify": "Rediger",
+ "name": "Navn",
+ "ok": "OK",
+ "open": "Åpne",
+ "or": "eller",
+ "rename": "Gi nytt navn",
+ "search": "Søk",
+ "size": "Størrelse",
+ "space": "Område",
+ "status": "Status",
+ "success": "Vellykket",
+ "type": "Type",
+ "unarchive": "Gjenopprett fra arkiv",
+ "validate": "Valider",
+ "verify": "Kontroller"
},
"FolderCreationButton": {
- "text": ""
+ "text": "Ny mappe"
},
"FolderCreationForm": {
- "title": ""
+ "title": "Opprett mappe"
},
"SpaceCardImageButton": {
"text": "Last opp bilde"
},
"FileTreePreviewModal": {
- "title": ""
+ "title": "Importer DMS-struktur fra"
}
-}
\ No newline at end of file
+}
diff --git a/src/services/ErrorService.js b/src/services/ErrorService.js
index 811667e99..221a8c9c6 100644
--- a/src/services/ErrorService.js
+++ b/src/services/ErrorService.js
@@ -87,6 +87,19 @@ const ERRORS = Object.freeze({
BCF_IMPORT_ERROR: "bcfImportError",
BCF_EXPORT_ERROR: "bcfExportError",
BCF_DELETE_ERROR: "bcfDeleteError",
+ NAMING_CONSTRAINTS_FETCH_ERROR: "namingConstraintsFetchError",
+ NAMING_CONSTRAINT_FETCH_ERROR: "namingConstraintFetchError",
+ NAMING_CONSTRAINT_CREATE_ERROR: "namingConstraintCreateError",
+ NAMING_CONSTRAINT_UPDATE_ERROR: "namingConstraintUpdateError",
+ NAMING_CONSTRAINT_DELETE_ERROR: "namingConstraintDeleteError",
+ NAMING_PART_LISTS_FETCH_ERROR: "namingPartListsFetchError",
+ NAMING_PART_LIST_CREATE_ERROR: "namingPartListCreateError",
+ NAMING_PART_LIST_UPDATE_ERROR: "namingPartListUpdateError",
+ NAMING_PART_LIST_DELETE_ERROR: "namingPartListDeleteError",
+ FOLDER_NAMING_CONSTRAINT_FETCH_ERROR: "folderNamingConstraintFetchError",
+ FOLDER_NAMING_CONSTRAINT_SET_ERROR: "folderNamingConstraintSetError",
+ FOLDER_NAMING_CONSTRAINT_DELETE_ERROR: "folderNamingConstraintDeleteError",
+ CONFLICTING_DOCUMENTS_FETCH_ERROR: "conflictingDocumentsFetchError",
});
class RuntimeError {
diff --git a/src/services/NamingConstraintService.js b/src/services/NamingConstraintService.js
new file mode 100644
index 000000000..9d7f94ea4
--- /dev/null
+++ b/src/services/NamingConstraintService.js
@@ -0,0 +1,325 @@
+import apiClient from "./api-client.js";
+import { ERRORS, RuntimeError, ErrorService } from "./ErrorService.js";
+
+/**
+ * Thrown when a strict folder naming constraint cannot be applied because
+ * existing documents in scope do not match the rule (API responds 409).
+ * `documents` holds the conflicting `LightDocument[]` returned by the API.
+ */
+class NamingConstraintConflictError {
+ constructor(documents) {
+ this.documents = documents;
+ }
+}
+
+/**
+ * Thrown when a value list cannot be deleted because constraints use it (API responds 409).
+ * `constraints` holds the `NamingConstraint[]` returned by the API.
+ */
+class NamingPartListInUseError {
+ constructor(constraints) {
+ this.constraints = constraints;
+ }
+}
+
+const isResponse = (error) => typeof Response !== "undefined" && error instanceof Response;
+
+// In-memory cache of effective folder rules, keyed by folder id, to avoid
+// re-fetching the rule on every rename/upload within the same folder.
+const folderRuleCache = new Map();
+
+class NamingConstraintService {
+ // --- Naming constraints catalog ------------------------------------------
+
+ async fetchNamingConstraints(project) {
+ try {
+ return await apiClient.collaborationApi.getNamingConstraints(project.cloud.id, project.id);
+ } catch (error) {
+ ErrorService.handleError(new RuntimeError(ERRORS.NAMING_CONSTRAINTS_FETCH_ERROR, error));
+ return [];
+ }
+ }
+
+ async fetchNamingConstraint(project, constraint) {
+ try {
+ return await apiClient.collaborationApi.getNamingConstraint(
+ project.cloud.id,
+ constraint.id,
+ project.id,
+ );
+ } catch (error) {
+ throw new RuntimeError(ERRORS.NAMING_CONSTRAINT_FETCH_ERROR, error);
+ }
+ }
+
+ async createNamingConstraint(project, payload) {
+ try {
+ return await apiClient.collaborationApi.createNamingConstraint(
+ project.cloud.id,
+ project.id,
+ payload,
+ );
+ } catch (error) {
+ throw new RuntimeError(ERRORS.NAMING_CONSTRAINT_CREATE_ERROR, error);
+ }
+ }
+
+ // Resolve the effective naming rule applying to a folder, with its `strict`
+ // flag. Returns null when no rule applies. Results are cached per folder id.
+ async getEffectiveFolderRule(project, folder) {
+ if (!folder?.id) return null;
+
+ if (folderRuleCache.has(folder.id)) {
+ return folderRuleCache.get(folder.id);
+ }
+
+ const folderConstraint = await this.fetchFolderNamingConstraint(project, folder);
+
+ const constraint = folderConstraint?.constraint ?? null;
+
+ const effective = constraint
+ ? {
+ id: constraint.id,
+ separator: constraint.separator,
+ parts: constraint.parts,
+ strict: constraint.strict,
+ name: constraint.name,
+ recursive: !!folderConstraint.recursive,
+ applied_on_folder_id: folderConstraint.applied_on_folder_id,
+ }
+ : null;
+
+ folderRuleCache.set(folder.id, effective);
+
+ return effective;
+ }
+
+ /**
+ * Updates a naming constraint. Never blocking: its `conflicting_documents`
+ * lists the documents now in conflict.
+ */
+ async updateNamingConstraint(project, constraint, payload) {
+ try {
+ const result = await apiClient.collaborationApi.updateNamingConstraint(
+ project.cloud.id,
+ constraint.id,
+ project.id,
+ payload,
+ );
+ this.clearFolderRuleCache();
+ return result;
+ } catch (error) {
+ throw new RuntimeError(ERRORS.NAMING_CONSTRAINT_UPDATE_ERROR, error);
+ }
+ }
+
+ async deleteNamingConstraint(project, constraint) {
+ try {
+ const response = await fetch(
+ `${ENV.VUE_APP_API_BASE_URL}/cloud/${project.cloud.id}/project/${project.id}/naming-constraint/${constraint.id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ ...apiClient.authHeader,
+ },
+ },
+ );
+ this.clearFolderRuleCache();
+ if (response.status === 200) {
+ const documents = await response.json();
+ throw new NamingConstraintConflictError(documents);
+ }
+ if (response.status !== 204) {
+ throw new RuntimeError(ERRORS.NAMING_CONSTRAINT_DELETE_ERROR, await response.text());
+ }
+ } catch (error) {
+ throw new RuntimeError(ERRORS.NAMING_CONSTRAINT_DELETE_ERROR, error);
+ }
+ }
+
+ // --- Naming part lists (value lists) -------------------------------------
+
+ async fetchNamingPartLists(project) {
+ try {
+ return await apiClient.collaborationApi.getNamingConstraintPartLists(
+ project.cloud.id,
+ project.id,
+ );
+ } catch (error) {
+ ErrorService.handleError(new RuntimeError(ERRORS.NAMING_PART_LISTS_FETCH_ERROR, error));
+ return [];
+ }
+ }
+
+ async createNamingPartList(project, payload) {
+ try {
+ return await apiClient.collaborationApi.createNamingConstraintPartList(
+ project.cloud.id,
+ project.id,
+ payload,
+ );
+ } catch (error) {
+ throw new RuntimeError(ERRORS.NAMING_PART_LIST_CREATE_ERROR, error);
+ }
+ }
+
+ /**
+ * Updates a value list. Never blocking: its `conflicting_documents` lists
+ * the documents now in conflict.
+ */
+ async updateNamingPartList(project, partList, payload) {
+ try {
+ const result = await apiClient.collaborationApi.updateNamingConstraintPartList(
+ project.cloud.id,
+ partList.id,
+ project.id,
+ payload,
+ );
+ this.clearFolderRuleCache();
+ return result;
+ } catch (error) {
+ throw new RuntimeError(ERRORS.NAMING_PART_LIST_UPDATE_ERROR, error);
+ }
+ }
+
+ /**
+ * Throws `NamingPartListInUseError` (with `constraints`) when the list is
+ * used by constraints (API responds 409).
+ */
+ async deleteNamingPartList(project, partList) {
+ try {
+ return await apiClient.collaborationApi.deleteNamingConstraintPartList(
+ project.cloud.id,
+ partList.id,
+ project.id,
+ );
+ } catch (error) {
+ if (isResponse(error) && error.status === 409) {
+ const constraints = await error.json();
+ throw new NamingPartListInUseError(constraints);
+ }
+ throw new RuntimeError(ERRORS.NAMING_PART_LIST_DELETE_ERROR, error);
+ }
+ }
+
+ // --- Folder naming constraint --------------------------------------------
+
+ /**
+ * Returns the effective `FolderNamingConstraint` for a folder (may be
+ * inherited from a recursive parent rule), or `null` when no rule applies
+ * (API responds 404).
+ */
+ async fetchFolderNamingConstraint(project, folder) {
+ try {
+ return await apiClient.collaborationApi.getFolderNamingConstraint(
+ project.cloud.id,
+ folder.id,
+ project.id,
+ );
+ } catch (error) {
+ if (isResponse(error) && error.status === 404) {
+ return null;
+ }
+ throw new RuntimeError(ERRORS.FOLDER_NAMING_CONSTRAINT_FETCH_ERROR, error);
+ }
+ }
+
+ /**
+ * Sets or replaces the naming constraint applied on a folder.
+ * Resolves with the `FolderNamingConstraint` (its `conflicting_documents`
+ * lists the non-blocking conflicts for non-strict rules).
+ * Throws `NamingConstraintConflictError` (with `documents`) when a strict
+ * rule conflicts with existing documents (API responds 409).
+ */
+ async setFolderNamingConstraint(project, folder, { constraint_id, recursive }) {
+ try {
+ const result = await apiClient.collaborationApi.setFolderNamingConstraint(
+ project.cloud.id,
+ folder.id,
+ project.id,
+ { constraint_id, recursive },
+ );
+ this.clearFolderRuleCache();
+ return result;
+ } catch (error) {
+ if (isResponse(error) && error.status === 409) {
+ const documents = await error.json();
+ throw new NamingConstraintConflictError(documents);
+ }
+ throw new RuntimeError(ERRORS.FOLDER_NAMING_CONSTRAINT_SET_ERROR, error);
+ }
+ }
+
+ /**
+ * Removes the direct naming constraint of a folder.
+ * Resolves with `[]` when removed cleanly (API responds 204), or with the
+ * `LightDocument[]` newly conflicting with an inherited parent rule (API
+ * responds 200 with a body).
+ */
+ async deleteFolderNamingConstraint(project, folder) {
+ try {
+ const documents = await apiClient.collaborationApi.deleteFolderNamingConstraint(
+ project.cloud.id,
+ folder.id,
+ project.id,
+ );
+ this.clearFolderRuleCache();
+ return documents ?? [];
+ } catch (error) {
+ if (isResponse(error) && error.status === 404) {
+ return [];
+ }
+ if (error instanceof SyntaxError) {
+ return [];
+ }
+ throw new RuntimeError(ERRORS.FOLDER_NAMING_CONSTRAINT_DELETE_ERROR, error);
+ }
+ }
+
+ // --- Conflicting documents -----------------------------------------------
+
+ /**
+ * Lists documents flagged with `naming_constraint_conflict = true`.
+ */
+ async fetchConflictingDocuments(project) {
+ try {
+ return await apiClient.collaborationApi.getDocuments(
+ project.cloud.id,
+ project.id,
+ undefined, // created_after
+ undefined, // created_before
+ undefined, // creator_email
+ undefined, // description
+ undefined, // description__contains
+ undefined, // description__endswith
+ undefined, // description__startswith
+ undefined, // file_name
+ undefined, // file_name__contains
+ undefined, // file_name__endswith
+ undefined, // file_name__startswith
+ undefined, // file_type
+ undefined, // has__visa
+ undefined, // id__in
+ undefined, // name
+ undefined, // name__contains
+ undefined, // name__endswith
+ undefined, // name__startswith
+ true, // naming_constraint_conflict
+ );
+ } catch (error) {
+ ErrorService.handleError(new RuntimeError(ERRORS.CONFLICTING_DOCUMENTS_FETCH_ERROR, error));
+ return [];
+ }
+ }
+
+ clearFolderRuleCache() {
+ folderRuleCache.clear();
+ }
+}
+
+const service = new NamingConstraintService();
+
+export { NamingConstraintConflictError, NamingPartListInUseError };
+
+export default service;
diff --git a/src/state/files.js b/src/state/files.js
index c7346155b..148120e66 100644
--- a/src/state/files.js
+++ b/src/state/files.js
@@ -1,6 +1,9 @@
import { reactive, readonly, toRefs } from "vue";
import FileService from "../services/FileService.js";
-import { FileStructureHandler, segregate } from "../utils/file-structure.js";
+import NamingConstraintService from "../services/NamingConstraintService.js";
+import { collectDescendants } from "../utils/file-tree.js";
+import { FileStructureHandler, isFolder, segregate } from "../utils/file-structure.js";
+import { matchName } from "../utils/naming-constraint.js";
import { useAuth } from "./auth.js";
import { useModels } from "./models.js";
@@ -11,8 +14,41 @@ const state = reactive({
let fileStructureHandler = new FileStructureHandler();
+/**
+ * The API flags `naming_constraint_conflict` with a naive split on the rule
+ * separator, so a name whose values contain the separator (e.g. "." as
+ * separator and a value ending with ".") is wrongly reported as conflicting.
+ * Re-check flagged documents with the client-side parser to stay consistent
+ * with the naming conflict modal.
+ */
+const fixNamingConflicts = async (project, fileStructure) => {
+ if (!Array.isArray(fileStructure?.children)) return;
+
+ const flagged = collectDescendants(
+ fileStructure,
+ file => !isFolder(file) && file.naming_constraint_conflict
+ );
+ const parentIds = [...new Set(flagged.map(file => file.parent_id))];
+ const rules = new Map(
+ await Promise.all(
+ parentIds.map(async id => [
+ id,
+ await NamingConstraintService.getEffectiveFolderRule(project, { id })
+ ])
+ )
+ );
+
+ for (const file of flagged) {
+ const rule = rules.get(file.parent_id);
+ if (rule && matchName(file.name, rule)) {
+ file.naming_constraint_conflict = false;
+ }
+ }
+};
+
const loadProjectFileStructure = async project => {
const fileStructure = await FileService.fetchFileStructure(project);
+ await fixNamingConflicts(project, fileStructure);
state.projectFileStructure = fileStructure;
fileStructureHandler.serialize(fileStructure);
return fileStructure;
diff --git a/src/state/naming-constraints.js b/src/state/naming-constraints.js
new file mode 100644
index 000000000..33496498f
--- /dev/null
+++ b/src/state/naming-constraints.js
@@ -0,0 +1,110 @@
+import { reactive, readonly, toRefs } from "vue";
+import NamingConstraintService from "../services/NamingConstraintService.js";
+
+const state = reactive({
+ namingConstraints: [],
+ namingPartLists: [],
+});
+
+// --- Naming constraints catalog --------------------------------------------
+
+const loadNamingConstraints = async (project) => {
+ const constraints = await NamingConstraintService.fetchNamingConstraints(project);
+ state.namingConstraints = constraints;
+ return constraints;
+};
+
+const createNamingConstraint = async (project, payload) => {
+ const constraint = await NamingConstraintService.createNamingConstraint(project, payload);
+ state.namingConstraints = [...state.namingConstraints, constraint];
+ return constraint;
+};
+
+const updateNamingConstraint = async (project, constraint, payload) => {
+ const updated = await NamingConstraintService.updateNamingConstraint(
+ project,
+ constraint,
+ payload,
+ );
+ state.namingConstraints = state.namingConstraints.map((item) =>
+ item.id === updated.id ? updated : item,
+ );
+ return updated;
+};
+
+const deleteNamingConstraint = async (project, constraint) => {
+ await NamingConstraintService.deleteNamingConstraint(project, constraint);
+ state.namingConstraints = state.namingConstraints.filter((item) => item.id !== constraint.id);
+};
+
+// --- Naming part lists (value lists) ---------------------------------------
+
+const loadNamingPartLists = async (project) => {
+ const partLists = await NamingConstraintService.fetchNamingPartLists(project);
+ state.namingPartLists = partLists;
+ return partLists;
+};
+
+const createNamingPartList = async (project, payload) => {
+ const partList = await NamingConstraintService.createNamingPartList(project, payload);
+ state.namingPartLists = [...state.namingPartLists, partList];
+ return partList;
+};
+
+const updateNamingPartList = async (project, partList, payload) => {
+ const updated = await NamingConstraintService.updateNamingPartList(project, partList, payload);
+ state.namingPartLists = state.namingPartLists.map((item) =>
+ item.id === updated.id ? updated : item,
+ );
+ return updated;
+};
+
+const deleteNamingPartList = async (project, partList) => {
+ await NamingConstraintService.deleteNamingPartList(project, partList);
+ state.namingPartLists = state.namingPartLists.filter((item) => item.id !== partList.id);
+};
+
+// --- Folder naming constraint ----------------------------------------------
+const fetchFolderNamingConstraint = async (project, folder) => {
+ return NamingConstraintService.fetchFolderNamingConstraint(project, folder);
+};
+
+const setFolderNamingConstraint = async (project, folder, payload) => {
+ return NamingConstraintService.setFolderNamingConstraint(project, folder, payload);
+};
+
+const deleteFolderNamingConstraint = async (project, folder) => {
+ return NamingConstraintService.deleteFolderNamingConstraint(project, folder);
+};
+
+const getEffectiveFolderRule = async (project, folder) => {
+ return NamingConstraintService.getEffectiveFolderRule(project, folder);
+};
+
+// --- Conflicting documents -------------------------------------------------
+
+const fetchConflictingDocuments = async (project) => {
+ return NamingConstraintService.fetchConflictingDocuments(project);
+};
+
+export function useNamingConstraints() {
+ const readOnlyState = readonly(state);
+ return {
+ // References
+ ...toRefs(readOnlyState),
+ // Methods
+ loadNamingConstraints,
+ createNamingConstraint,
+ updateNamingConstraint,
+ deleteNamingConstraint,
+ loadNamingPartLists,
+ createNamingPartList,
+ updateNamingPartList,
+ deleteNamingPartList,
+ fetchFolderNamingConstraint,
+ setFolderNamingConstraint,
+ deleteFolderNamingConstraint,
+ getEffectiveFolderRule,
+ fetchConflictingDocuments,
+ };
+}
diff --git a/src/utils/naming-constraint.js b/src/utils/naming-constraint.js
new file mode 100644
index 000000000..fcdded14f
--- /dev/null
+++ b/src/utils/naming-constraint.js
@@ -0,0 +1,309 @@
+/**
+ * Client-side helpers to work with naming-constraint rules without round-trips
+ * to the API (e.g. validate a name before starting a large upload).
+ *
+ * A rule has the shape:
+ * {
+ * separator: "_",
+ * parts: [
+ * { type: "values_in", elements: ["ARC", "STR"] },
+ * { type: "bounded", min_value: 1, max_value: 99 },
+ * { type: "n_chars", max_length: 12 }
+ * ]
+ * }
+ *
+ * Names are validated on the file name without its extension, the same way the
+ * backend evaluates them.
+ */
+
+const PART_TYPES = Object.freeze({
+ VALUES_IN: "values_in",
+ BOUNDED: "bounded",
+ N_CHARS: "n_chars",
+});
+
+/**
+ * Strip the extension from a file name.
+ *
+ * @param {String} name
+ * @returns {String}
+ */
+function stripExtension(name) {
+ const dotIndex = name.lastIndexOf(".");
+ return dotIndex > 0 ? name.slice(0, dotIndex) : name;
+}
+
+/**
+ * Get the file extension.
+ *
+ * @param {String} name
+ * @returns {String}
+ */
+function getExtension(name) {
+ const dotIndex = name.lastIndexOf(".");
+ return dotIndex > 0 ? name.slice(dotIndex) : "";
+}
+
+/**
+ * Check whether a single segment matches a rule part.
+ *
+ * @param {Object} part
+ * @param {String} segment
+ * @returns {Boolean}
+ */
+function matchPart(part, segment) {
+ switch (part?.type) {
+ case PART_TYPES.VALUES_IN:
+ return Array.isArray(part.elements) && part.elements.includes(segment);
+
+ case PART_TYPES.BOUNDED: {
+ if (!/^\d+$/.test(segment)) {
+ return false;
+ }
+
+ const value = Number(segment);
+
+ return value >= part.min_value && value <= part.max_value;
+ }
+
+ case PART_TYPES.N_CHARS:
+ return segment.length > 0 && segment.length <= part.max_length;
+
+ default:
+ return false;
+ }
+}
+
+/**
+ * Parse a filename according to a naming constraint rule.
+ *
+ * This cannot rely on String.split() because the separator can also be part
+ * of a valid value. For example, with "-" as separator:
+ *
+ * rule.parts[0] = { type: "values_in", elements: ["A-B"] }
+ *
+ * The parser therefore tries valid values for each part and only treats the
+ * separator as a delimiter when the remaining parts can also be matched.
+ *
+ * @param {String} name
+ * @param {Object|null} rule
+ * @returns {String[]|null}
+ */
+function parseNameParts(name, rule) {
+ if (!rule?.parts?.length || typeof name !== "string" || !name.length) {
+ return null;
+ }
+
+ const baseName = stripExtension(name);
+ const separator = rule.separator ?? "";
+
+ if (!separator) {
+ return rule.parts.length === 1 ? [baseName] : null;
+ }
+
+ const parsePart = (value, partIndex) => {
+ const part = rule.parts[partIndex];
+ const isLastPart = partIndex === rule.parts.length - 1;
+
+ for (let end = 1; end <= value.length; end++) {
+ const candidate = value.slice(0, end);
+
+ if (!matchPart(part, candidate)) {
+ continue;
+ }
+
+ const remaining = value.slice(end);
+
+ if (isLastPart) {
+ if (!remaining) {
+ return [candidate];
+ }
+
+ continue;
+ }
+
+ if (!remaining.startsWith(separator)) {
+ continue;
+ }
+
+ const parsedRemaining = parsePart(remaining.slice(separator.length), partIndex + 1);
+
+ if (parsedRemaining) {
+ return [candidate, ...parsedRemaining];
+ }
+ }
+
+ return null;
+ };
+
+ return parsePart(baseName, 0);
+}
+
+/**
+ * Check whether a file name matches a naming-constraint rule.
+ *
+ * Returns true when there is no rule (nothing to enforce).
+ *
+ * @param {String} name
+ * @param {Object|null} rule
+ * @returns {Boolean}
+ */
+function matchName(name, rule) {
+ if (!rule?.parts?.length) {
+ return true;
+ }
+
+ return parseNameParts(name, rule) !== null;
+}
+
+/**
+ * Build a human-friendly example segment for a single rule part.
+ *
+ * Uses the part name when available.
+ *
+ * @param {Object} part
+ * @returns {String}
+ */
+function buildPartExample(part) {
+ return part?.name ?? "";
+}
+
+/**
+ * Build a human-friendly example name from a rule.
+ *
+ * @param {Object|null} rule
+ * @returns {String}
+ */
+function buildExample(rule) {
+ if (!rule?.parts?.length) {
+ return "";
+ }
+
+ const separator = rule.separator ?? "";
+
+ return `${rule.parts.map(buildPartExample).join(separator)}.ext`;
+}
+
+/**
+ * Build default editable values for a rule.
+ *
+ * @param {String} basename
+ * @param {Object} rule
+ * @returns {String[]}
+ */
+function buildDefaultValues(basename, rule) {
+ if (!Array.isArray(rule?.parts)) {
+ return [];
+ }
+
+ return rule.parts.map((part) => {
+ switch (part.type) {
+ case PART_TYPES.VALUES_IN:
+ return part.elements?.[0] ?? "";
+
+ case PART_TYPES.BOUNDED:
+ return part.min_value ?? "";
+
+ case PART_TYPES.N_CHARS:
+ return basename.slice(0, part.max_length);
+
+ default:
+ return "";
+ }
+ });
+}
+
+/**
+ * Split a filename into editable values and extension.
+ *
+ * @param {String} name
+ * @param {Object|null} rule
+ * @returns {{values: String[], extension: String}}
+ */
+function splitName(name, rule) {
+ const extension = typeof name === "string" ? getExtension(name) : "";
+
+ if (!Array.isArray(rule?.parts) || rule.parts.length === 0) {
+ return {
+ values: [],
+ extension,
+ };
+ }
+
+ const parsedValues = parseNameParts(name, rule);
+
+ if (parsedValues) {
+ return {
+ values: parsedValues,
+ extension,
+ };
+ }
+
+ return {
+ values: buildDefaultValues(stripExtension(name), rule),
+ extension,
+ };
+}
+
+/**
+ * Left-pad a bounded value according to its max_value.
+ *
+ * max_value=999 -> 001
+ * max_value=99 -> 01
+ *
+ * @param {String|Number} value
+ * @param {Object} part
+ * @returns {String}
+ */
+function padBoundedValue(value, part) {
+ if (value === "" || value === null || value === undefined) {
+ return "";
+ }
+
+ const digits = String(part.max_value).length;
+
+ return String(value).padStart(digits, "0");
+}
+
+/**
+ * Build a filename from rule values.
+ *
+ * @param {String[]} values
+ * @param {Object} rule
+ * @param {String} extension
+ * @returns {String}
+ */
+function buildName(values, rule, extension = "") {
+ if (!rule) {
+ return "";
+ }
+
+ const separator = rule.separator ?? "";
+
+ const name = values
+ .map((value, index) => {
+ const part = rule.parts[index];
+
+ if (part?.type === PART_TYPES.BOUNDED) {
+ return padBoundedValue(value, part);
+ }
+
+ return value;
+ })
+ .join(separator);
+
+ return name + extension;
+}
+
+export {
+ PART_TYPES,
+ matchName,
+ matchPart,
+ parseNameParts,
+ buildExample,
+ buildPartExample,
+ stripExtension,
+ splitName,
+ padBoundedValue,
+ buildName,
+};
diff --git a/src/views/project-board/project-files/ProjectFiles.vue b/src/views/project-board/project-files/ProjectFiles.vue
index d1309c30b..68ad8ad62 100644
--- a/src/views/project-board/project-files/ProjectFiles.vue
+++ b/src/views/project-board/project-files/ProjectFiles.vue
@@ -6,8 +6,8 @@
name: routeNames.projectGroups,
params: {
spaceID: project.cloud.id,
- projectID: project.id
- }
+ projectID: project.id,
+ },
}"
>
@@ -57,13 +57,15 @@ import AppLink from "../../../components/specific/app/app-link/AppLink.vue";
import AppLoading from "../../../components/specific/app/app-loading/AppLoading.vue";
import AppSlotContent from "../../../components/specific/app/app-slot/AppSlotContent.js";
import FilesManager from "../../../components/specific/files/files-manager/FilesManager.vue";
+import NamingConstraintsManager from "../../../components/specific/files/naming-constraint/NamingConstraintsManager.vue";
+import { useAppSidePanel } from "../../../components/specific/app/app-side-panel/app-side-panel.js";
export default {
components: {
AppLink,
AppLoading,
AppSlotContent,
- FilesManager
+ FilesManager,
},
setup() {
const { isProjectAdmin } = useUser();
@@ -71,14 +73,19 @@ export default {
const { currentProject } = useProjects();
const { loadProjectModels } = useModels();
const { projectFileStructure, loadProjectFileStructure } = useFiles();
+ const { openSidePanel } = useAppSidePanel();
- const reloadData = debounce(async () => {
- await Promise.all([
+ const reloadData = async () => {
+ const [, fileStructure] = await Promise.all([
loadSpaceSubInfo(currentSpace.value),
loadProjectFileStructure(currentProject.value),
- loadProjectModels(currentProject.value)
+ loadProjectModels(currentProject.value),
]);
- }, 1000);
+
+ return fileStructure;
+ };
+
+ const reloadDataDebounced = debounce(reloadData, 1000);
return {
// References
@@ -89,10 +96,11 @@ export default {
// Methods
isProjectAdmin,
reloadData,
+ reloadDataDebounced,
// Responsive breakpoints
- ...useStandardBreakpoints()
+ ...useStandardBreakpoints(),
};
- }
+ },
};
diff --git a/tests/unit/utils/naming-constraint.spec.js b/tests/unit/utils/naming-constraint.spec.js
new file mode 100644
index 000000000..f563d26c3
--- /dev/null
+++ b/tests/unit/utils/naming-constraint.spec.js
@@ -0,0 +1,123 @@
+import {
+ matchName,
+ matchPart,
+ buildExample,
+ buildPartExample,
+ stripExtension,
+ PART_TYPES
+} from "../../../src/utils/naming-constraint.js";
+
+const rule = {
+ separator: "_",
+ parts: [
+ { type: "values_in", elements: ["ARC", "STR"] },
+ { type: "bounded", min_value: 1, max_value: 99 },
+ { type: "n_chars", max_length: 12 }
+ ]
+};
+
+describe("Naming constraint - stripExtension", () => {
+ it("Should remove the extension", () => {
+ expect(stripExtension("ARC_01_plan.ifc")).toBe("ARC_01_plan");
+ expect(stripExtension("My.File.Name.pdf")).toBe("My.File.Name");
+ });
+
+ it("Should keep names without extension", () => {
+ expect(stripExtension("ARC_01_plan")).toBe("ARC_01_plan");
+ expect(stripExtension(".gitignore")).toBe(".gitignore");
+ });
+});
+
+describe("Naming constraint - matchPart", () => {
+ it("values_in matches only listed elements", () => {
+ const part = { type: PART_TYPES.VALUES_IN, elements: ["ARC", "STR"] };
+ expect(matchPart(part, "ARC")).toBe(true);
+ expect(matchPart(part, "STR")).toBe(true);
+ expect(matchPart(part, "MEP")).toBe(false);
+ expect(matchPart(part, "arc")).toBe(false);
+ });
+
+ it("bounded matches integers within range", () => {
+ const part = { type: PART_TYPES.BOUNDED, min_value: 1, max_value: 99 };
+ expect(matchPart(part, "1")).toBe(true);
+ expect(matchPart(part, "99")).toBe(true);
+ expect(matchPart(part, "0")).toBe(false);
+ expect(matchPart(part, "100")).toBe(false);
+ expect(matchPart(part, "12a")).toBe(false);
+ expect(matchPart(part, "")).toBe(false);
+ });
+
+ it("n_chars matches non-empty strings up to max length", () => {
+ const part = { type: PART_TYPES.N_CHARS, max_length: 3 };
+ expect(matchPart(part, "a")).toBe(true);
+ expect(matchPart(part, "abc")).toBe(true);
+ expect(matchPart(part, "abcd")).toBe(false);
+ expect(matchPart(part, "")).toBe(false);
+ });
+
+ it("Unknown part type does not match", () => {
+ expect(matchPart({ type: "unknown" }, "x")).toBe(false);
+ expect(matchPart(undefined, "x")).toBe(false);
+ });
+});
+
+describe("Naming constraint - matchName", () => {
+ it("Should match valid names", () => {
+ expect(matchName("ARC_01_plan", rule)).toBe(true);
+ expect(matchName("STR_99_a", rule)).toBe(true);
+ expect(matchName("ARC_5_groundfloor.ifc", rule)).toBe(true);
+ });
+
+ it("Should reject names with wrong segment count", () => {
+ expect(matchName("ARC_01", rule)).toBe(false);
+ expect(matchName("ARC_01_plan_extra", rule)).toBe(false);
+ });
+
+ it("Should reject names with invalid segments", () => {
+ expect(matchName("MEP_01_plan", rule)).toBe(false);
+ expect(matchName("ARC_0_plan", rule)).toBe(false);
+ expect(matchName("ARC_100_plan", rule)).toBe(false);
+ expect(matchName("ARC_01_thisnameistoolong", rule)).toBe(false);
+ });
+
+ it("Should treat empty or missing rule as always valid", () => {
+ expect(matchName("anything", null)).toBe(true);
+ expect(matchName("anything", { separator: "_", parts: [] })).toBe(true);
+ });
+
+ it("Should reject empty names against a real rule", () => {
+ expect(matchName("", rule)).toBe(false);
+ });
+
+ it("Should support an empty separator (single part)", () => {
+ const singleRule = {
+ separator: "",
+ parts: [{ type: "values_in", elements: ["ARC"] }]
+ };
+ expect(matchName("ARC", singleRule)).toBe(true);
+ expect(matchName("ARC.ifc", singleRule)).toBe(true);
+ expect(matchName("STR", singleRule)).toBe(false);
+ });
+});
+
+describe("Naming constraint - buildExample", () => {
+ it("Should build a human-friendly example", () => {
+ expect(buildExample(rule)).toBe("ARC_[1-99]_XXX");
+ });
+
+ it("Should use placeholder when values_in has no elements", () => {
+ const r = { separator: "-", parts: [{ type: "values_in", elements: [] }] };
+ expect(buildExample(r)).toBe("...");
+ });
+
+ it("Should return an empty string for empty rules", () => {
+ expect(buildExample(null)).toBe("");
+ expect(buildExample({ separator: "_", parts: [] })).toBe("");
+ });
+
+ it("buildPartExample handles each type", () => {
+ expect(buildPartExample({ type: "values_in", elements: ["A"] })).toBe("A");
+ expect(buildPartExample({ type: "bounded", min_value: 1, max_value: 9 })).toBe("[1-9]");
+ expect(buildPartExample({ type: "n_chars", max_length: 4 })).toBe("XXX");
+ });
+});