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" > @@ -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 @@ -
- +
+ + + {{ $t("t.validate") }} + + + + +
+ - - {{ $t("t.validate") }} - - - -
-
- +
+ + + +
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 @@
+
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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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"); + }); +});