diff --git a/package-lock.json b/package-lock.json
index de2a56a07..a74b3930b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,7 +11,7 @@
"@bimdata/bcf-components": "6.7.7",
"@bimdata/components": "1.10.1",
"@bimdata/design-system": "2.4.2",
- "@bimdata/typescript-fetch-api-client": "10.41.0",
+ "@bimdata/typescript-fetch-api-client": "10.44.1",
"@bimdata/viewer": "2.17.1-alpha.fragments.3",
"@paddle/paddle-js": "^1.6.4",
"async": "^3.2.6",
@@ -1882,9 +1882,9 @@
"integrity": "sha512-pldzXuZEL4oUzjxnd0+AVlM551yDag4mXJCehKAvfScdhq4bt1Iks17ux1yk4Ppt+dFJTb2EO0vYBuipaBRq9Q=="
},
"node_modules/@bimdata/typescript-fetch-api-client": {
- "version": "10.41.0",
- "resolved": "https://registry.npmjs.org/@bimdata/typescript-fetch-api-client/-/typescript-fetch-api-client-10.41.0.tgz",
- "integrity": "sha512-/CCvzpbuyb1pSkvuKvskiSscic8Z4GplkKHBn48RGfsCe6t2yzIakg78tQp3wnFSpRBSr2BBOXYhuq44B9MzpA=="
+ "version": "10.44.1",
+ "resolved": "https://registry.npmjs.org/@bimdata/typescript-fetch-api-client/-/typescript-fetch-api-client-10.44.1.tgz",
+ "integrity": "sha512-N5kHmrnk4CchkYQr3Elvdik3YaGGefbcp9x+djsYxXyRZlCK6SKHYlkjDp0IUZuY6QKU/36NWuH+q7OuVppcRw=="
},
"node_modules/@bimdata/viewer": {
"version": "2.17.1-alpha.fragments.3",
@@ -1895,7 +1895,7 @@
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.12.0.tgz",
"integrity": "sha512-B/XlCaFIP8LOwzo+bz5uFzATYokcwCKQcghqnlfwSmM5eX/qTkvDBnDPs+gXtX/RyjxJ4DRikECcPJbyALA8FA==",
- "dev": true,
+ "devOptional": true,
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@colors/colors": {
@@ -1963,7 +1963,6 @@
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
- "dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -1975,7 +1974,6 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "dev": true,
"license": "0BSD",
"optional": true
},
@@ -1983,7 +1981,6 @@
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
- "dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -1994,7 +1991,6 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "dev": true,
"license": "0BSD",
"optional": true
},
@@ -2002,7 +1998,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
- "dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -2013,7 +2008,6 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "dev": true,
"license": "0BSD",
"optional": true
},
@@ -2815,7 +2809,6 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
- "dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -3008,7 +3001,6 @@
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
"integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
- "dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -3048,7 +3040,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3069,7 +3060,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3090,7 +3080,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3111,7 +3100,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3132,7 +3120,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3153,7 +3140,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3174,7 +3160,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3195,7 +3180,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3216,7 +3200,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3237,7 +3220,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3258,7 +3240,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3279,7 +3260,6 @@
"cpu": [
"ia32"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3300,7 +3280,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3390,7 +3369,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3407,7 +3385,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3424,7 +3401,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3441,7 +3417,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3458,7 +3433,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3475,7 +3449,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3492,7 +3465,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3509,7 +3481,6 @@
"cpu": [
"ppc64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3526,7 +3497,6 @@
"cpu": [
"s390x"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3543,7 +3513,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3560,7 +3529,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3577,7 +3545,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3594,7 +3561,6 @@
"cpu": [
"wasm32"
],
- "dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -3613,7 +3579,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3630,7 +3595,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4301,7 +4265,6 @@
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
- "dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -4312,7 +4275,6 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "dev": true,
"license": "0BSD",
"optional": true
},
@@ -4425,7 +4387,7 @@
"version": "25.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
"integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"undici-types": ">=7.24.0 <7.24.7"
@@ -6053,7 +6015,7 @@
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz",
"integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/colors": {
@@ -7724,7 +7686,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -8014,7 +7975,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -8226,7 +8187,7 @@
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz",
"integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/import-fresh": {
@@ -8390,7 +8351,7 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -8426,7 +8387,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
@@ -9498,7 +9459,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -9519,7 +9479,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -9540,7 +9499,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -9561,7 +9519,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -9582,7 +9539,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -9603,7 +9559,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -9624,7 +9579,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -9645,7 +9599,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -9666,7 +9619,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -9687,7 +9639,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -9708,7 +9659,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -10562,7 +10512,6 @@
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
- "dev": true,
"license": "MIT",
"optional": true
},
@@ -13819,7 +13768,7 @@
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
- "dev": true,
+ "devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
@@ -13829,7 +13778,7 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "dev": true,
+ "devOptional": true,
"license": "0BSD"
},
"node_modules/safe-buffer": {
@@ -13864,7 +13813,6 @@
"version": "1.100.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.100.0.tgz",
"integrity": "sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ==",
- "dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -13886,7 +13834,7 @@
"version": "1.100.0",
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.100.0.tgz",
"integrity": "sha512-Ut8wlQSk19tm7jMK6mz6cF1+e+E7tUnW2tM02zQDPnOTcVbV8qCQG8UWxZkkNlY50+hV3hqP24OOkUlMz8xBpw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@bufbuild/protobuf": "^2.5.0",
@@ -13934,7 +13882,6 @@
"!riscv64",
"!x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -13948,7 +13895,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -13965,7 +13911,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -13982,7 +13927,6 @@
"cpu": [
"riscv64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -13999,7 +13943,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -14016,7 +13959,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -14033,7 +13975,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -14050,7 +13991,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -14067,7 +14007,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -14084,7 +14023,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -14101,7 +14039,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -14118,7 +14055,6 @@
"cpu": [
"riscv64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -14135,7 +14071,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -14152,7 +14087,6 @@
"cpu": [
"riscv64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -14169,7 +14103,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -14183,7 +14116,6 @@
"version": "1.100.0",
"resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.100.0.tgz",
"integrity": "sha512-c+naBgWId4MIpToXcI0DgqetjdAkwTTAxFAuOaBz7HUXLdyG1oZRrEvSsbe41nEdQOKH0vgofVFCeSQgoXOG9A==",
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -14203,7 +14135,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -14220,7 +14151,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -14234,7 +14164,7 @@
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
@@ -15367,7 +15297,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz",
"integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"sync-message-port": "^1.0.0"
@@ -15380,7 +15310,7 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.2.0.tgz",
"integrity": "sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=16.0.0"
@@ -15837,7 +15767,7 @@
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/unicode-canonical-property-names-ecmascript": {
@@ -16106,7 +16036,7 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
"integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/verror": {
diff --git a/package.json b/package.json
index 3305d15b5..ae8639527 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,7 @@
"@bimdata/bcf-components": "6.7.7",
"@bimdata/components": "1.10.1",
"@bimdata/design-system": "2.4.2",
- "@bimdata/typescript-fetch-api-client": "10.41.0",
+ "@bimdata/typescript-fetch-api-client": "10.44.1",
"@bimdata/viewer": "2.17.1-alpha.fragments.3",
"@paddle/paddle-js": "^1.6.4",
"async": "^3.2.6",
diff --git a/src/components/specific/files/all-files-table/AllFilesTable.vue b/src/components/specific/files/all-files-table/AllFilesTable.vue
index cbeca91a1..5064933d4 100644
--- a/src/components/specific/files/all-files-table/AllFilesTable.vue
+++ b/src/components/specific/files/all-files-table/AllFilesTable.vue
@@ -218,6 +218,7 @@
@download="$emit('download', file)"
@file-clicked="$emit('file-clicked', file)"
@manage-access="$emit('manage-access', file)"
+ @manage-naming-rule="$emit('manage-naming-rule', file)"
@open-tag-manager="$emit('open-tag-manager', file)"
@open-versioning-manager="$emit('open-versioning-manager', file)"
@open-visa-manager="$emit('open-visa-manager', file)"
@@ -288,6 +289,7 @@ export default {
"file-clicked",
"go-folders-view",
"manage-access",
+ "manage-naming-rule",
"open-tag-manager",
"open-versioning-manager",
"open-visa-manager",
diff --git a/src/components/specific/files/files-manager/FilesManager.vue b/src/components/specific/files/files-manager/FilesManager.vue
index 243a31375..17d9c9571 100644
--- a/src/components/specific/files/files-manager/FilesManager.vue
+++ b/src/components/specific/files/files-manager/FilesManager.vue
@@ -11,6 +11,7 @@
:initialSearchText="searchText"
@update:searchText="searchText = $event"
@upload-files="uploadFiles"
+ @manage-naming-conflicts="openNamingConflictsModal"
/>
@@ -88,6 +89,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 +113,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 +208,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 +230,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";
@@ -280,7 +287,8 @@ export default {
const { createModel, createPhotosphere, deleteModels } = useModels();
const { fetchToValidateVisas, fetchCreatedVisas } = useVisa();
-
+ const { getEffectiveFolderRule } =
+ useNamingConstraints();
const currentFolder = ref(null);
const currentFiles = ref([]);
const toValidateVisas = ref([]);
@@ -337,20 +345,57 @@ 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, 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 fromEvent = await getFilesFromEvent(event);
+ const files = fromEvent.files;
+ folders = fromEvent.folders;
+
+ const rule = await getEffectiveFolderRule(props.project, folder);
+ const invalidFiles = rule?.rule
+ ? files.filter((file) => !matchName(file.name, rule.rule))
+ : [];
+
+ if (invalidFiles.length > 0) {
+ openModal({
+ component: NamingConflictModal,
+ props: {
+ project: props.project,
+ documents: invalidFiles.map((file, i) => ({ id: `upload-${i}`, name: file.name })),
+ rule,
+ persistChanges: false,
+ onClose: closeModal,
+ onConfirm: ({ renamed, deleted }) => {
+ const deletedIds = new Set(deleted.map((d) => d.id));
+ const renamedById = new Map(renamed.map((r) => [r.id, r.name]));
+ const finalFiles = invalidFiles
+ .map((file, i) => ({ file, id: `upload-${i}` }))
+ .filter(({ id }) => !deletedIds.has(id))
+ .map(({ file, id }) => {
+ const name = renamedById.get(id);
+ return name ? new File([file], name, { type: file.type }) : file;
+ });
+ const validFiles = files.filter((file) => matchName(file.name, rule.rule));
+ proceedUpload([...validFiles, ...finalFiles], folder);
+ },
+ },
+ });
+ return;
+ }
+
+ proceedUpload(files, folder);
+ };
const loadingFileIds = ref([]);
const isCreatingModels = ref(false);
@@ -544,6 +589,50 @@ export default {
}, 100);
};
+ const openFolderNamingConstraintManager = (folder) => {
+ openSidePanel("right", {
+ component: FolderNamingConstraintManager,
+ props: {
+ project: props.project,
+ folder,
+ },
+ });
+ };
+
+ const openNamingConflictsModal = async () => {
+ const conflicting = allFiles.value.filter(
+ (file) => file.naming_constraint_conflict,
+ );
+ if (conflicting.length === 0) {
+ pushNotification({
+ type: "success",
+ title: t("NamingConstraint.noConflictsTitle"),
+ message: t("NamingConstraint.noConflictsMessage"),
+ });
+ return;
+ }
+ const documents = await Promise.all(
+ conflicting.map(async (file) => {
+ const folder = allFolders.value.find((f) => f.id === file.parent_id);
+ const effective = folder
+ ? await getEffectiveFolderRule(props.project, folder)
+ : null;
+ return { ...file, namingRule: effective?.rule ?? null };
+ }),
+ );
+ openModal({
+ component: NamingConflictModal,
+ props: {
+ project: props.project,
+ documents,
+ allFolders: allFolders.value,
+ rule: null,
+ onClose: closeModal,
+ onConfirm: () => emit("file-updated"),
+ },
+ });
+ };
+
const visasLoading = ref(false);
const openVisaManager = (file) => {
onTabChange(filesTabs[2]);
@@ -833,6 +922,8 @@ export default {
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..c1bf7f204 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
@@ -118,6 +118,8 @@ import {
import { useFiles } from "../../../../../state/files.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
@@ -157,7 +159,7 @@ 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();
@@ -176,6 +178,14 @@ export default {
};
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 = [];
@@ -192,6 +202,17 @@ export default {
);
}
+ 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"),
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..cd47c905a 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
@@ -82,6 +82,7 @@ export default {
"download",
"file-clicked",
"manage-access",
+ "manage-naming-rule",
"open-tag-manager",
"open-versioning-manager",
"open-visa-manager",
@@ -96,6 +97,8 @@ export default {
const isOpen = ref(false);
const menuItems = shallowRef([]);
+ let current_key = 0;
+
const openMenu = () => {
if (!props.parent) return;
@@ -103,7 +106,7 @@ export default {
if (!isFolder(props.file)) {
menuItems.value.push({
- key: 1,
+ key: current_key++,
icon: "preview",
text: "FileActionsCell.previewModelButtonText",
color: "primary",
@@ -114,7 +117,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,7 +128,7 @@ 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),
@@ -133,7 +136,7 @@ export default {
});
} else {
menuItems.value.push({
- key: 4,
+ key: current_key++,
iconComponent: RemoveModelsIcon,
text: "FileActionsCell.removeModelButtonText",
action: () => onClick("remove-model"),
@@ -143,7 +146,7 @@ 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),
@@ -152,7 +155,7 @@ export default {
}
menuItems.value.push({
- key: 5,
+ key: current_key++,
icon: "edit",
text: "t.rename",
disabled: !hasAdminPerm(props.project, props.file),
@@ -168,31 +171,38 @@ export default {
if (isFolder(props.file) && isProjectAdmin(props.project)) {
menuItems.value.push({
- key: 7,
+ key: current_key++,
icon: "key",
text: "FileActionsCell.manageAccessButtonText",
action: () => onClick("manage-access"),
divider: true,
});
+ menuItems.value.push({
+ key: current_key++,
+ iconComponent: "BIMDataIconLock",
+ text: "NamingConstraint.folderRuleMenuItem",
+ action: () => onClick("manage-naming-rule"),
+ dataTestId: "btn-manage-naming-rule",
+ });
}
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 +212,7 @@ export default {
}
menuItems.value.push({
- key: 11,
+ key: current_key++,
icon: "delete",
text: "t.delete",
color: "high",
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 e74af99e5..8aaa200d8 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
@@ -13,7 +13,7 @@
@keyup.esc.stop="closeUpdateForm"
@keyup.enter.stop="renameFile"
:error="hasError"
- :errorMessage="$t('t.invalidName')"
+ :errorMessage="errorMessage"
margin="0"
/>
+
+
+
props.file?.history_count > 0);
const renameFile = debounce(async () => {
if (fileName.value) {
+ const rule = isFolder(props.file)
+ ? null
+ : await getEffectiveFolderRule(props.project, {
+ id: props.file.parent_id,
+ });
+ if (rule?.rule && !matchName(fileName.value, rule.rule)) {
+ if (rule.strict) {
+ hasError.value = true;
+ errorMessage.value = t("t.invalidNameFormat", {
+ example: buildExample(rule.rule),
+ });
+ nameInput.value.focus();
+ return;
+ }
+ pushNotification({
+ type: "warning",
+ title: t("NamingConstraint.applyRuleWarningTitle"),
+ message: t("t.invalidNameFormat", {
+ example: buildExample(rule.rule),
+ }),
+ });
+ }
try {
loading.value = true;
await updateFiles(props.project, [
@@ -119,6 +162,7 @@ export default {
}
} else {
hasError.value = true;
+ errorMessage.value = t("t.invalidName");
nameInput.value.focus();
}
}, 500);
@@ -132,6 +176,7 @@ export default {
const closeUpdateForm = () => {
loading.value = false;
hasError.value = false;
+ errorMessage.value = "";
showUpdateForm.value = false;
emit("close");
};
@@ -162,6 +207,7 @@ export default {
// References
fileName,
hasError,
+ errorMessage,
loading,
nameInput,
showUpdateForm,
diff --git a/src/components/specific/files/folder-table/FoldersTable.vue b/src/components/specific/files/folder-table/FoldersTable.vue
index 76c4e7a55..ea2f94be7 100644
--- a/src/components/specific/files/folder-table/FoldersTable.vue
+++ b/src/components/specific/files/folder-table/FoldersTable.vue
@@ -96,6 +96,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)"
@@ -172,6 +173,7 @@ export default {
"file-clicked",
"file-uploaded",
"manage-access",
+ "manage-naming-rule",
"open-tag-manager",
"open-versioning-manager",
"open-visa-manager",
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..573e1893b
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/FolderNamingConstraintManager.vue
@@ -0,0 +1,187 @@
+
+
+
+
+
+
+
diff --git a/src/components/specific/files/naming-constraint/NamingConflictModal.scss b/src/components/specific/files/naming-constraint/NamingConflictModal.scss
new file mode 100644
index 000000000..e63b68ac1
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/NamingConflictModal.scss
@@ -0,0 +1,39 @@
+.naming-conflict-modal {
+ &__content {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ text-align: left;
+ }
+
+ &__rule {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+
+ &__name {
+ font-weight: 700;
+ font-size: 12px;
+ color: var(--color-primary);
+ }
+
+ &__chip {
+ padding: 3px 7px;
+ background-color: #f0f5ff;
+ color: #205dbd;
+ border-radius: 6px;
+ font-size: 12px;
+ }
+ }
+
+ &__intro,
+ &__warning {
+ margin: 0;
+ font-size: 14px;
+ color: var(--color-granite);
+ }
+
+ &__warning {
+ font-weight: 700;
+ }
+}
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..bffab1c2d
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/NamingConflictModal.vue
@@ -0,0 +1,123 @@
+
+
+
+
+ {{ $t("NamingConstraint.managerTitle") }}
+
+
+
+
+ {{ rule.name }}
+
+ {{ buildExample(rule.rule) }}
+
+
+
+ {{ $t("NamingConstraint.conflictModalIntro") }}
+
+
+ {{ $t("NamingConstraint.conflictModalWarning") }}
+
+
+
+
+
+
+ {{ $t("t.cancel") }}
+
+
+ {{ $t("t.confirm") }}
+
+
+
+
+
+
+
+
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..aa17cad51
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/NamingConstraintsManager.vue
@@ -0,0 +1,187 @@
+
+
+
+
+
+
+
diff --git a/src/components/specific/files/naming-constraint/conflicting-documents-list/ConflictingDocumentsList.scss b/src/components/specific/files/naming-constraint/conflicting-documents-list/ConflictingDocumentsList.scss
new file mode 100644
index 000000000..51c56274c
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/conflicting-documents-list/ConflictingDocumentsList.scss
@@ -0,0 +1,65 @@
+.conflicting-documents-list {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ max-height: 320px;
+ overflow-y: auto;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+
+ &__item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 10px 16px;
+ background-color: var(--color-tertiary-lightest);
+ border-radius: 6px;
+ box-shadow: var(--box-shadow);
+
+ &__name {
+ flex: 1;
+ font-size: 13px;
+ color: var(--color-primary);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ &__info {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ }
+
+ &__example {
+ font-size: 11px;
+ color: var(--color-granite-light);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ &__path {
+ font-size: 11px;
+ }
+
+ &__input {
+ flex: 1;
+ }
+
+ &--editing {
+ background-color: var(--color-white);
+ }
+
+ &--invalid &__name {
+ color: var(--color-high);
+ }
+
+ &--deleted &__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..5b442929e
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/conflicting-documents-list/ConflictingDocumentsList.vue
@@ -0,0 +1,168 @@
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ names[doc.id] ?? doc.name }}
+
+
+
+ {{ buildExample(effectiveRule(doc)) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..20dc11623
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/folder-naming-constraint-selector/FolderNamingConstraintSelector.vue
@@ -0,0 +1,399 @@
+
+
+
+
+ {{ $t("NamingConstraint.rulesSectionTitle") }}
+
+
+
+ {{ $t("NamingConstraint.addRuleButton") }}
+
+
+
+
+
+
+
+
+ {{ $t("NamingConstraint.rulesEmptyTitle") }}
+
+
+ {{ $t("NamingConstraint.rulesEmptyText") }}
+
+
+
+ {{ $t("NamingConstraint.addRuleButton") }}
+
+
+
+
+
+ -
+
+
+
+
+ {{ constraint.name }}
+
+
+
+ {{ buildExample(constraint.rule) }}
+
+
+ {{ $t("NamingConstraint.strictBadge") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..f3fbb6f87
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/naming-constraint-form/NamingConstraintForm.vue
@@ -0,0 +1,391 @@
+
+
+
+
+
+
+
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..1d3d77279
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/naming-constraint-form/RuleBuilder.vue
@@ -0,0 +1,338 @@
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t("NamingConstraint.addPartButton") }}
+
+
+
+
+
+
+
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..c50b3d85b
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/naming-constraints-list/NamingConstraintsList.vue
@@ -0,0 +1,293 @@
+
+
+
+
+ {{ $t("NamingConstraint.rulesSectionTitle") }}
+
+
+
+ {{ $t("NamingConstraint.addRuleButton") }}
+
+
+
+
+
+
+
+
+ {{ $t("NamingConstraint.rulesEmptyTitle") }}
+
+
+ {{ $t("NamingConstraint.rulesEmptyText") }}
+
+
+
+ {{ $t("NamingConstraint.addRuleButton") }}
+
+
+
+
+
+
+
+ {{ $t("NamingConstraint.manageListsButton") }}
+
+
+
+
+
+
+
diff --git a/src/components/specific/files/naming-constraint/naming-parts-template-form/NamingPartsTemplateForm.vue b/src/components/specific/files/naming-constraint/naming-parts-template-form/NamingPartsTemplateForm.vue
new file mode 100644
index 000000000..893a09137
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/naming-parts-template-form/NamingPartsTemplateForm.vue
@@ -0,0 +1,283 @@
+
+
+
+
+
+
+
diff --git a/src/components/specific/files/naming-constraint/naming-parts-templates-list/NamingPartsTemplatesList.vue b/src/components/specific/files/naming-constraint/naming-parts-templates-list/NamingPartsTemplatesList.vue
new file mode 100644
index 000000000..aa88345e7
--- /dev/null
+++ b/src/components/specific/files/naming-constraint/naming-parts-templates-list/NamingPartsTemplatesList.vue
@@ -0,0 +1,262 @@
+
+
+
+
+ {{ $t("NamingConstraint.listsSectionTitle") }}
+
+
+
+ {{ $t("NamingConstraint.createListButton") }}
+
+
+
+
+
+
+
+
+ {{ $t("NamingConstraint.listsEmptyTitle") }}
+
+
+ {{ $t("NamingConstraint.listsEmptyText") }}
+
+
+
+ {{ $t("NamingConstraint.createListButton") }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/i18n/lang/de.json b/src/i18n/lang/de.json
index 18fc239e7..7bd75713c 100644
--- a/src/i18n/lang/de.json
+++ b/src/i18n/lang/de.json
@@ -1,4 +1,43 @@
{
+ "NamingConstraint": {
+ "managerTitle": "",
+ "constraintsTab": "",
+ "templatesTab": "",
+ "constraintsListCreateButton": "",
+ "constraintsListEmpty": "",
+ "templatesListCreateButton": "",
+ "templatesListEmpty": "",
+ "strictBadge": "",
+ "nonStrictBadge": "",
+ "createConstraintTitle": "",
+ "updateConstraintTitle": "",
+ "createTemplateTitle": "",
+ "updateTemplateTitle": "",
+ "nameLabel": "",
+ "strictLabel": "",
+ "strictHelp": "",
+ "previewLabel": "",
+ "ruleSectionTitle": "",
+ "separatorLabel": "",
+ "addPartButton": "",
+ "emptyParts": "",
+ "partTypeLabel": "",
+ "partTypeValuesIn": "",
+ "partTypeBounded": "",
+ "partTypeNChars": "",
+ "elementsLabel": "",
+ "elementsHelp": "",
+ "loadFromTemplateLabel": "",
+ "minLabel": "",
+ "maxLabel": "",
+ "maxLengthLabel": "",
+ "templateElementsLabel": "",
+ "templateElementsHelp": "",
+ "emptyElementsError": "",
+ "invalidBoundsError": "",
+ "deleteConstraintConfirm": "",
+ "deleteTemplateConfirm": ""
+ },
"OidcCallbackError": {
"message": "Bei der Authentifizierung ist ein Fehler aufgetreten...",
"retryButtonText": "Nochmals versuchen"
@@ -440,6 +479,19 @@
"invitationViewAcceptError": "Die Einladung kann nicht angenommen werden",
"folderFetchFolder": "Fehler beim Abrufen von Ordnerinformationen.",
"bcfDeleteError": "Das Löschen ist fehlgeschlagen...",
+ "namingConstraintsFetchError": "",
+ "namingConstraintFetchError": "",
+ "namingConstraintCreateError": "",
+ "namingConstraintUpdateError": "",
+ "namingConstraintDeleteError": "",
+ "namingPartsTemplatesFetchError": "",
+ "namingPartsTemplateCreateError": "",
+ "namingPartsTemplateUpdateError": "",
+ "namingPartsTemplateDeleteError": "",
+ "folderNamingConstraintFetchError": "",
+ "folderNamingConstraintSetError": "",
+ "folderNamingConstraintDeleteError": "",
+ "conflictingDocumentsFetchError": "",
"groupImportError": "Fehler beim Importieren von Gruppen",
"bcfExportXlsxError": "Der Excel-Export ist fehlgeschlagen...",
"invitationViewDeclineError": "Einladung kann nicht abgelehnt werden"
@@ -971,4 +1023,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 9635601d3..253c79323 100644
--- a/src/i18n/lang/en.json
+++ b/src/i18n/lang/en.json
@@ -1,4 +1,91 @@
{
+ "NamingConstraint": {
+ "managerTitle": "Naming convention management",
+ "constraintsTab": "Naming rules",
+ "templatesTab": "Value lists",
+ "rulesSectionTitle": "Rules list",
+ "addRuleButton": "add a rule",
+ "searchPlaceholder": "Search",
+ "rulesEmptyTitle": "No rule added",
+ "rulesEmptyText": "You don't have any rule yet, start by adding one.",
+ "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.",
+ "constraintsListCreateButton": "New naming rule",
+ "constraintsListEmpty": "No naming rule yet.",
+ "templatesListCreateButton": "New value list",
+ "templatesListEmpty": "No value list yet.",
+ "strictBadge": "Strict",
+ "nonStrictBadge": "Non-strict",
+ "createConstraintTitle": "Creating a rule",
+ "updateConstraintTitle": "Editing a rule",
+ "createTemplateTitle": "Creating my list",
+ "updateTemplateTitle": "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",
+ "separatorLabel": "Separator",
+ "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",
+ "elementsLabel": "Allowed values",
+ "elementsHelp": "Comma-separated list of allowed values.",
+ "loadFromTemplateLabel": "Load from a list",
+ "minLabel": "Min",
+ "maxLabel": "Max",
+ "maxLengthLabel": "Max length",
+ "saveRuleButton": "Save the rule",
+ "listNameStep": "List name",
+ "listNamePlaceholder": "List name",
+ "addElementsStep": "Add elements",
+ "elementNamePlaceholder": "Element name",
+ "removeElementButton": "Remove",
+ "addElementButton": "Add an element",
+ "saveListButton": "Save the list",
+ "templateElementsLabel": "Values",
+ "templateElementsHelp": "Comma-separated list of values.",
+ "emptyElementsError": "Provide at least one value.",
+ "invalidBoundsError": "Max must be greater than or equal to min.",
+ "deleteConstraintConfirm": "Delete this naming rule?",
+ "deleteTemplateConfirm": "Delete this list?",
+ "folderRuleMenuItem": "Naming convention",
+ "recursiveLabel": "Recursive rule",
+ "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.",
+ "applyRuleConflictTitle": "Some files do not match",
+ "applyRuleConflictMessage": "The rule was applied but {count} existing file(s) do not match it.",
+ "applyRuleBlockedTitle": "Strict rule cannot be applied",
+ "applyRuleBlockedMessage": "{count} existing file(s) do not match this strict rule. Rename them first.",
+ "applyRuleError": "The naming convention could not be applied.",
+ "noFolderRuleSelection": "Select a rule to apply.",
+ "applyRuleWarningTitle": "Name does not match the rule",
+ "uploadBlocked": "{count} file(s) were not uploaded because their name does not match the rule (e.g. {example}).",
+ "uploadWarning": "{count} file(s) do not match the naming rule (e.g. {example}).",
+ "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",
+ "renameButton": "Rename",
+ "renameFilePlaceholder": "Rename the file",
+ "renameConflictsMenuItem": "Rename conflicting files",
+ "noConflictsTitle": "No conflicting files",
+ "noConflictsMessage": "All files match their folder naming convention."
+ },
"OidcCallbackError": {
"message": "An error occured during authentication...",
"retryButtonText": "Try Again"
@@ -510,6 +597,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",
+ "namingPartsTemplatesFetchError": "Unable to retrieve naming templates",
+ "namingPartsTemplateCreateError": "Unable to create this naming template",
+ "namingPartsTemplateUpdateError": "Unable to update this naming template",
+ "namingPartsTemplateDeleteError": "Unable to delete this naming template",
+ "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"
@@ -982,6 +1082,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",
diff --git a/src/i18n/lang/es.json b/src/i18n/lang/es.json
index 01fb515f9..f8a9d6459 100644
--- a/src/i18n/lang/es.json
+++ b/src/i18n/lang/es.json
@@ -1,4 +1,43 @@
{
+ "NamingConstraint": {
+ "managerTitle": "",
+ "constraintsTab": "",
+ "templatesTab": "",
+ "constraintsListCreateButton": "",
+ "constraintsListEmpty": "",
+ "templatesListCreateButton": "",
+ "templatesListEmpty": "",
+ "strictBadge": "",
+ "nonStrictBadge": "",
+ "createConstraintTitle": "",
+ "updateConstraintTitle": "",
+ "createTemplateTitle": "",
+ "updateTemplateTitle": "",
+ "nameLabel": "",
+ "strictLabel": "",
+ "strictHelp": "",
+ "previewLabel": "",
+ "ruleSectionTitle": "",
+ "separatorLabel": "",
+ "addPartButton": "",
+ "emptyParts": "",
+ "partTypeLabel": "",
+ "partTypeValuesIn": "",
+ "partTypeBounded": "",
+ "partTypeNChars": "",
+ "elementsLabel": "",
+ "elementsHelp": "",
+ "loadFromTemplateLabel": "",
+ "minLabel": "",
+ "maxLabel": "",
+ "maxLengthLabel": "",
+ "templateElementsLabel": "",
+ "templateElementsHelp": "",
+ "emptyElementsError": "",
+ "invalidBoundsError": "",
+ "deleteConstraintConfirm": "",
+ "deleteTemplateConfirm": ""
+ },
"OidcCallbackError": {
"message": "Ha ocurrido un error de autenticación...",
"retryButtonText": "Intente de nuevo"
@@ -440,6 +479,19 @@
"invitationViewAcceptError": "No se puede aceptar la invitación",
"folderFetchFolder": "Error al recuperar la información de la carpeta",
"bcfDeleteError": "Eliminación fallida...",
+ "namingConstraintsFetchError": "",
+ "namingConstraintFetchError": "",
+ "namingConstraintCreateError": "",
+ "namingConstraintUpdateError": "",
+ "namingConstraintDeleteError": "",
+ "namingPartsTemplatesFetchError": "",
+ "namingPartsTemplateCreateError": "",
+ "namingPartsTemplateUpdateError": "",
+ "namingPartsTemplateDeleteError": "",
+ "folderNamingConstraintFetchError": "",
+ "folderNamingConstraintSetError": "",
+ "folderNamingConstraintDeleteError": "",
+ "conflictingDocumentsFetchError": "",
"groupImportError": "Error al importar grupos",
"bcfExportXlsxError": "La exportación a Excél ha fracasado",
"invitationViewDeclineError": "No se permite rechazar la invitación"
@@ -971,4 +1023,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 6804246bc..e4aec6b7e 100644
--- a/src/i18n/lang/fr.json
+++ b/src/i18n/lang/fr.json
@@ -1,4 +1,91 @@
{
+ "NamingConstraint": {
+ "managerTitle": "Gestion des conventions de nommage",
+ "constraintsTab": "Règles de nommage",
+ "templatesTab": "Listes de valeurs",
+ "rulesSectionTitle": "Liste des règles",
+ "addRuleButton": "ajouter une règle",
+ "searchPlaceholder": "Rechercher",
+ "rulesEmptyTitle": "Pas de règle ajoutée",
+ "rulesEmptyText": "Vous n'avez aucune règle pour l'instant, commencez par en ajouter une.",
+ "manageListsButton": "Gérer les listes",
+ "listsSectionTitle": "Gestion des listes",
+ "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.",
+ "constraintsListCreateButton": "Nouvelle règle de nommage",
+ "constraintsListEmpty": "Aucune règle de nommage pour le moment.",
+ "templatesListCreateButton": "Nouvelle liste de valeurs",
+ "templatesListEmpty": "Aucune liste de valeurs pour le moment.",
+ "strictBadge": "Stricte",
+ "nonStrictBadge": "Non stricte",
+ "createConstraintTitle": "Création d'une règle",
+ "updateConstraintTitle": "Modification d'une règle",
+ "createTemplateTitle": "Création de ma liste",
+ "updateTemplateTitle": "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",
+ "separatorLabel": "Séparateur",
+ "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",
+ "elementsLabel": "Valeurs autorisées",
+ "elementsHelp": "Liste de valeurs autorisées séparées par des virgules.",
+ "loadFromTemplateLabel": "Charger depuis une liste",
+ "minLabel": "Min",
+ "maxLabel": "Max",
+ "maxLengthLabel": "Longueur max",
+ "saveRuleButton": "Enregistrer la règle",
+ "listNameStep": "Nom de la liste",
+ "listNamePlaceholder": "Nom de la liste",
+ "addElementsStep": "Ajouter des éléments",
+ "elementNamePlaceholder": "Nom de l'élément",
+ "removeElementButton": "Effacer",
+ "addElementButton": "Ajouter un élément",
+ "saveListButton": "Enregistrer la liste",
+ "templateElementsLabel": "Valeurs",
+ "templateElementsHelp": "Liste de valeurs séparées par des virgules.",
+ "emptyElementsError": "Indiquez au moins une valeur.",
+ "invalidBoundsError": "Le max doit être supérieur ou égal au min.",
+ "deleteConstraintConfirm": "Supprimer cette règle de nommage ?",
+ "deleteTemplateConfirm": "Supprimer cette liste ?",
+ "folderRuleMenuItem": "Convention de nommage",
+ "recursiveLabel": "Règle récursive",
+ "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 convention de nommage a été appliquée au dossier.",
+ "applyRuleConflictTitle": "Certains fichiers ne correspondent pas",
+ "applyRuleConflictMessage": "La règle a été appliquée mais {count} fichier(s) existant(s) ne la respectent pas.",
+ "applyRuleBlockedTitle": "Règle stricte non applicable",
+ "applyRuleBlockedMessage": "{count} fichier(s) existant(s) ne respectent pas cette règle stricte. Renommez-les d'abord.",
+ "applyRuleError": "La convention de nommage n'a pas pu être appliquée.",
+ "noFolderRuleSelection": "Sélectionnez une règle à appliquer.",
+ "applyRuleWarningTitle": "Le nom ne respecte pas la règle",
+ "uploadBlocked": "{count} fichier(s) n'ont pas été importés car leur nom ne respecte pas la règle (ex. {example}).",
+ "uploadWarning": "{count} fichier(s) ne respectent pas la convention de nommage (ex. {example}).",
+ "conflictTooltip": "Ce fichier ne respecte pas la convention de nommage du dossier.",
+ "conflictModalIntro": "Renommez ici les fichiers qui ne correspondent pas à la règle stricte que vous avez programmé.",
+ "conflictModalWarning": "Renommez ou supprimez les documents en conflit pour continuer",
+ "renameButton": "Renommer",
+ "renameFilePlaceholder": "Renommer le fichier",
+ "renameConflictsMenuItem": "Renommer les fichiers en conflit",
+ "noConflictsTitle": "Aucun fichier en conflit",
+ "noConflictsMessage": "Tous les fichiers respectent la convention de nommage de leur dossier."
+ },
"t": {
"add": "Ajouter",
"amount": "Montant",
@@ -31,6 +118,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",
@@ -612,6 +700,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",
+ "namingPartsTemplatesFetchError": "Impossible de récupérer les modèles de nommage",
+ "namingPartsTemplateCreateError": "Impossible de créer ce modèle de nommage",
+ "namingPartsTemplateUpdateError": "Impossible de modifier ce modèle de nommage",
+ "namingPartsTemplateDeleteError": "Impossible de supprimer ce modèle 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",
diff --git a/src/i18n/lang/it.json b/src/i18n/lang/it.json
index 6663e729d..f88275823 100644
--- a/src/i18n/lang/it.json
+++ b/src/i18n/lang/it.json
@@ -1,4 +1,43 @@
{
+ "NamingConstraint": {
+ "managerTitle": "",
+ "constraintsTab": "",
+ "templatesTab": "",
+ "constraintsListCreateButton": "",
+ "constraintsListEmpty": "",
+ "templatesListCreateButton": "",
+ "templatesListEmpty": "",
+ "strictBadge": "",
+ "nonStrictBadge": "",
+ "createConstraintTitle": "",
+ "updateConstraintTitle": "",
+ "createTemplateTitle": "",
+ "updateTemplateTitle": "",
+ "nameLabel": "",
+ "strictLabel": "",
+ "strictHelp": "",
+ "previewLabel": "",
+ "ruleSectionTitle": "",
+ "separatorLabel": "",
+ "addPartButton": "",
+ "emptyParts": "",
+ "partTypeLabel": "",
+ "partTypeValuesIn": "",
+ "partTypeBounded": "",
+ "partTypeNChars": "",
+ "elementsLabel": "",
+ "elementsHelp": "",
+ "loadFromTemplateLabel": "",
+ "minLabel": "",
+ "maxLabel": "",
+ "maxLengthLabel": "",
+ "templateElementsLabel": "",
+ "templateElementsHelp": "",
+ "emptyElementsError": "",
+ "invalidBoundsError": "",
+ "deleteConstraintConfirm": "",
+ "deleteTemplateConfirm": ""
+ },
"OidcCallbackError": {
"message": "Si è verificato un errore durante l'autenticazione...",
"retryButtonText": "Riprova"
@@ -361,6 +400,19 @@
"invitationViewDeclineError": "Impossibile rifiutare l'invito",
"folderFetchFolder": "Errore durante il recupero delle informazioni sulla cartella",
"bcfDeleteError": "Eliminazione non riuscita...",
+ "namingConstraintsFetchError": "",
+ "namingConstraintFetchError": "",
+ "namingConstraintCreateError": "",
+ "namingConstraintUpdateError": "",
+ "namingConstraintDeleteError": "",
+ "namingPartsTemplatesFetchError": "",
+ "namingPartsTemplateCreateError": "",
+ "namingPartsTemplateUpdateError": "",
+ "namingPartsTemplateDeleteError": "",
+ "folderNamingConstraintFetchError": "",
+ "folderNamingConstraintSetError": "",
+ "folderNamingConstraintDeleteError": "",
+ "conflictingDocumentsFetchError": "",
"groupImportError": "Impossibile importare i gruppi"
},
"ProjectStatusBadge": {
@@ -855,4 +907,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 2b839d2b7..98390d3c0 100644
--- a/src/i18n/lang/nl.json
+++ b/src/i18n/lang/nl.json
@@ -1,4 +1,43 @@
{
+ "NamingConstraint": {
+ "managerTitle": "",
+ "constraintsTab": "",
+ "templatesTab": "",
+ "constraintsListCreateButton": "",
+ "constraintsListEmpty": "",
+ "templatesListCreateButton": "",
+ "templatesListEmpty": "",
+ "strictBadge": "",
+ "nonStrictBadge": "",
+ "createConstraintTitle": "",
+ "updateConstraintTitle": "",
+ "createTemplateTitle": "",
+ "updateTemplateTitle": "",
+ "nameLabel": "",
+ "strictLabel": "",
+ "strictHelp": "",
+ "previewLabel": "",
+ "ruleSectionTitle": "",
+ "separatorLabel": "",
+ "addPartButton": "",
+ "emptyParts": "",
+ "partTypeLabel": "",
+ "partTypeValuesIn": "",
+ "partTypeBounded": "",
+ "partTypeNChars": "",
+ "elementsLabel": "",
+ "elementsHelp": "",
+ "loadFromTemplateLabel": "",
+ "minLabel": "",
+ "maxLabel": "",
+ "maxLengthLabel": "",
+ "templateElementsLabel": "",
+ "templateElementsHelp": "",
+ "emptyElementsError": "",
+ "invalidBoundsError": "",
+ "deleteConstraintConfirm": "",
+ "deleteTemplateConfirm": ""
+ },
"OidcCallbackError": {
"message": "Er heeft zich een fout voorgedaan bij de authenticatie",
"retryButtonText": ""
@@ -357,6 +396,19 @@
"invitationViewDeclineError": "",
"folderFetchFolder": "",
"bcfDeleteError": "",
+ "namingConstraintsFetchError": "",
+ "namingConstraintFetchError": "",
+ "namingConstraintCreateError": "",
+ "namingConstraintUpdateError": "",
+ "namingConstraintDeleteError": "",
+ "namingPartsTemplatesFetchError": "",
+ "namingPartsTemplateCreateError": "",
+ "namingPartsTemplateUpdateError": "",
+ "namingPartsTemplateDeleteError": "",
+ "folderNamingConstraintFetchError": "",
+ "folderNamingConstraintSetError": "",
+ "folderNamingConstraintDeleteError": "",
+ "conflictingDocumentsFetchError": "",
"groupImportError": ""
},
"ProjectStatusBadge": {
@@ -851,4 +903,4 @@
"FileTreePreviewModal": {
"title": ""
}
-}
\ No newline at end of file
+}
diff --git a/src/i18n/lang/no.json b/src/i18n/lang/no.json
index da63aa636..9c69eceec 100644
--- a/src/i18n/lang/no.json
+++ b/src/i18n/lang/no.json
@@ -1,4 +1,43 @@
{
+ "NamingConstraint": {
+ "managerTitle": "",
+ "constraintsTab": "",
+ "templatesTab": "",
+ "constraintsListCreateButton": "",
+ "constraintsListEmpty": "",
+ "templatesListCreateButton": "",
+ "templatesListEmpty": "",
+ "strictBadge": "",
+ "nonStrictBadge": "",
+ "createConstraintTitle": "",
+ "updateConstraintTitle": "",
+ "createTemplateTitle": "",
+ "updateTemplateTitle": "",
+ "nameLabel": "",
+ "strictLabel": "",
+ "strictHelp": "",
+ "previewLabel": "",
+ "ruleSectionTitle": "",
+ "separatorLabel": "",
+ "addPartButton": "",
+ "emptyParts": "",
+ "partTypeLabel": "",
+ "partTypeValuesIn": "",
+ "partTypeBounded": "",
+ "partTypeNChars": "",
+ "elementsLabel": "",
+ "elementsHelp": "",
+ "loadFromTemplateLabel": "",
+ "minLabel": "",
+ "maxLabel": "",
+ "maxLengthLabel": "",
+ "templateElementsLabel": "",
+ "templateElementsHelp": "",
+ "emptyElementsError": "",
+ "invalidBoundsError": "",
+ "deleteConstraintConfirm": "",
+ "deleteTemplateConfirm": ""
+ },
"OidcCallbackError": {
"message": "En feil oppsto under autentiseringen...",
"retryButtonText": ""
@@ -357,6 +396,19 @@
"invitationViewDeclineError": "",
"folderFetchFolder": "",
"bcfDeleteError": "",
+ "namingConstraintsFetchError": "",
+ "namingConstraintFetchError": "",
+ "namingConstraintCreateError": "",
+ "namingConstraintUpdateError": "",
+ "namingConstraintDeleteError": "",
+ "namingPartsTemplatesFetchError": "",
+ "namingPartsTemplateCreateError": "",
+ "namingPartsTemplateUpdateError": "",
+ "namingPartsTemplateDeleteError": "",
+ "folderNamingConstraintFetchError": "",
+ "folderNamingConstraintSetError": "",
+ "folderNamingConstraintDeleteError": "",
+ "conflictingDocumentsFetchError": "",
"groupImportError": ""
},
"ProjectStatusBadge": {
@@ -851,4 +903,4 @@
"FileTreePreviewModal": {
"title": ""
}
-}
\ No newline at end of file
+}
diff --git a/src/services/ErrorService.js b/src/services/ErrorService.js
index 811667e99..1452e9390 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_PARTS_TEMPLATES_FETCH_ERROR: "namingPartsTemplatesFetchError",
+ NAMING_PARTS_TEMPLATE_CREATE_ERROR: "namingPartsTemplateCreateError",
+ NAMING_PARTS_TEMPLATE_UPDATE_ERROR: "namingPartsTemplateUpdateError",
+ NAMING_PARTS_TEMPLATE_DELETE_ERROR: "namingPartsTemplateDeleteError",
+ 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..55e7cb0bb
--- /dev/null
+++ b/src/services/NamingConstraintService.js
@@ -0,0 +1,256 @@
+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;
+ }
+}
+
+const isResponse = error =>
+ typeof Response !== "undefined" && error instanceof Response;
+
+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);
+ }
+ }
+
+ /**
+ * Updates a naming constraint.
+ * Resolves with the `NamingConstraint` (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 updateNamingConstraint(project, constraint, payload) {
+ try {
+ return await apiClient.collaborationApi.updateNamingConstraint(
+ project.cloud.id,
+ constraint.id,
+ project.id,
+ payload
+ );
+ } catch (error) {
+ if (isResponse(error) && error.status === 409) {
+ const documents = await error.json();
+ throw new NamingConstraintConflictError(documents);
+ }
+ throw new RuntimeError(ERRORS.NAMING_CONSTRAINT_UPDATE_ERROR, error);
+ }
+ }
+
+ async deleteNamingConstraint(project, constraint) {
+ try {
+ return await apiClient.collaborationApi.deleteNamingConstraint(
+ project.cloud.id,
+ constraint.id,
+ project.id
+ );
+ } catch (error) {
+ throw new RuntimeError(ERRORS.NAMING_CONSTRAINT_DELETE_ERROR, error);
+ }
+ }
+
+ // --- Naming parts templates ----------------------------------------------
+
+ async fetchNamingPartsTemplates(project) {
+ try {
+ return await apiClient.collaborationApi.getNamingPartsTemplates(
+ project.cloud.id,
+ project.id
+ );
+ } catch (error) {
+ ErrorService.handleError(
+ new RuntimeError(ERRORS.NAMING_PARTS_TEMPLATES_FETCH_ERROR, error)
+ );
+ return [];
+ }
+ }
+
+ async createNamingPartsTemplate(project, payload) {
+ try {
+ return await apiClient.collaborationApi.createNamingPartsTemplate(
+ project.cloud.id,
+ project.id,
+ payload
+ );
+ } catch (error) {
+ throw new RuntimeError(ERRORS.NAMING_PARTS_TEMPLATE_CREATE_ERROR, error);
+ }
+ }
+
+ async updateNamingPartsTemplate(project, template, payload) {
+ try {
+ return await apiClient.collaborationApi.updateNamingPartsTemplate(
+ project.cloud.id,
+ template.id,
+ project.id,
+ payload
+ );
+ } catch (error) {
+ throw new RuntimeError(ERRORS.NAMING_PARTS_TEMPLATE_UPDATE_ERROR, error);
+ }
+ }
+
+ async deleteNamingPartsTemplate(project, template) {
+ try {
+ return await apiClient.collaborationApi.deleteNamingPartsTemplate(
+ project.cloud.id,
+ template.id,
+ project.id
+ );
+ } catch (error) {
+ throw new RuntimeError(ERRORS.NAMING_PARTS_TEMPLATE_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 {
+ return await apiClient.collaborationApi.setFolderNamingConstraint(
+ project.cloud.id,
+ folder.id,
+ project.id,
+ { constraint_id, recursive }
+ );
+ } 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
+ );
+ return documents ?? [];
+ } catch (error) {
+ 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 [];
+ }
+ }
+}
+
+const service = new NamingConstraintService();
+
+export { NamingConstraintConflictError };
+
+export default service;
diff --git a/src/state/naming-constraints.js b/src/state/naming-constraints.js
new file mode 100644
index 000000000..333b23e31
--- /dev/null
+++ b/src/state/naming-constraints.js
@@ -0,0 +1,163 @@
+import { reactive, readonly, toRefs } from "vue";
+import NamingConstraintService, {
+ NamingConstraintConflictError
+} from "../services/NamingConstraintService.js";
+
+const state = reactive({
+ namingConstraints: [],
+ namingPartsTemplates: []
+});
+
+// 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();
+
+// --- 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 parts templates ------------------------------------------------
+
+const loadNamingPartsTemplates = async project => {
+ const templates = await NamingConstraintService.fetchNamingPartsTemplates(
+ project
+ );
+ state.namingPartsTemplates = templates;
+ return templates;
+};
+
+const createNamingPartsTemplate = async (project, payload) => {
+ const template = await NamingConstraintService.createNamingPartsTemplate(
+ project,
+ payload
+ );
+ state.namingPartsTemplates = [...state.namingPartsTemplates, template];
+ return template;
+};
+
+const updateNamingPartsTemplate = async (project, template, payload) => {
+ const updated = await NamingConstraintService.updateNamingPartsTemplate(
+ project,
+ template,
+ payload
+ );
+ state.namingPartsTemplates = state.namingPartsTemplates.map(item =>
+ item.id === updated.id ? updated : item
+ );
+ return updated;
+};
+
+const deleteNamingPartsTemplate = async (project, template) => {
+ await NamingConstraintService.deleteNamingPartsTemplate(project, template);
+ state.namingPartsTemplates = state.namingPartsTemplates.filter(
+ item => item.id !== template.id
+ );
+};
+
+// --- Folder naming constraint ----------------------------------------------
+
+const fetchFolderNamingConstraint = async (project, folder) => {
+ return NamingConstraintService.fetchFolderNamingConstraint(project, folder);
+};
+
+const setFolderNamingConstraint = async (project, folder, payload) => {
+ const result = await NamingConstraintService.setFolderNamingConstraint(
+ project,
+ folder,
+ payload
+ );
+ folderRuleCache.clear();
+ return result;
+};
+
+const deleteFolderNamingConstraint = async (project, folder) => {
+ const result = await NamingConstraintService.deleteFolderNamingConstraint(
+ project,
+ folder
+ );
+ folderRuleCache.clear();
+ return result;
+};
+
+// 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.
+const getEffectiveFolderRule = async (project, folder) => {
+ if (!folder?.id) return null;
+ if (folderRuleCache.has(folder.id)) {
+ return folderRuleCache.get(folder.id);
+ }
+ const folderConstraint = await NamingConstraintService.fetchFolderNamingConstraint(
+ project,
+ folder
+ );
+ const constraint = folderConstraint?.constraint ?? null;
+ const effective = constraint
+ ? { rule: constraint.rule, strict: constraint.strict, name: constraint.name }
+ : null;
+ folderRuleCache.set(folder.id, effective);
+ return effective;
+};
+
+// --- Conflicting documents -------------------------------------------------
+
+const fetchConflictingDocuments = async project => {
+ return NamingConstraintService.fetchConflictingDocuments(project);
+};
+
+export { NamingConstraintConflictError };
+
+export function useNamingConstraints() {
+ const readOnlyState = readonly(state);
+ return {
+ // References
+ ...toRefs(readOnlyState),
+ // Methods
+ loadNamingConstraints,
+ createNamingConstraint,
+ updateNamingConstraint,
+ deleteNamingConstraint,
+ loadNamingPartsTemplates,
+ createNamingPartsTemplate,
+ updateNamingPartsTemplate,
+ deleteNamingPartsTemplate,
+ 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..abbcf2069
--- /dev/null
+++ b/src/utils/naming-constraint.js
@@ -0,0 +1,121 @@
+/**
+ * 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 (e.g. "ARC_01.ifc" -> "ARC_01").
+ * Names without an extension are returned unchanged.
+ *
+ * @param {String} name
+ * @returns {String}
+ */
+function stripExtension(name) {
+ const dotIndex = name.lastIndexOf(".");
+ return dotIndex > 0 ? name.slice(0, dotIndex) : name;
+}
+
+/**
+ * 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;
+ }
+}
+
+/**
+ * Check whether a file name matches a naming-constraint rule.
+ * Returns `true` when there is no rule (nothing to enforce).
+ *
+ * @param {String} name file name (with or without extension)
+ * @param {Object|null} rule naming-constraint rule
+ * @returns {Boolean}
+ */
+function matchName(name, rule) {
+ if (!rule || !Array.isArray(rule.parts) || rule.parts.length === 0) {
+ return true;
+ }
+ if (typeof name !== "string" || name.length === 0) return false;
+
+ const baseName = stripExtension(name);
+ const separator = rule.separator ?? "";
+ const segments = separator === "" ? [baseName] : baseName.split(separator);
+
+ if (segments.length !== rule.parts.length) return false;
+
+ return rule.parts.every((part, index) => matchPart(part, segments[index]));
+}
+
+/**
+ * Build a human-friendly example segment for a single rule part.
+ *
+ * - values_in -> first element if defined, otherwise "..."
+ * - n_chars -> "XXX"
+ * - bounded -> "[MIN-MAX]"
+ *
+ * @param {Object} part
+ * @returns {String}
+ */
+function buildPartExample(part) {
+ switch (part?.type) {
+ case PART_TYPES.VALUES_IN:
+ return part.elements?.[0] ?? "...";
+ case PART_TYPES.BOUNDED:
+ return `[${part.min_value}-${part.max_value}]`;
+ case PART_TYPES.N_CHARS:
+ return "XXX";
+ default:
+ return "";
+ }
+}
+
+/**
+ * Build a human-friendly example name from a rule, to preview the expected
+ * format in the UI (e.g. "ARC_[1-99]_XXX").
+ *
+ * @param {Object|null} rule
+ * @returns {String}
+ */
+function buildExample(rule) {
+ if (!rule || !Array.isArray(rule.parts) || rule.parts.length === 0) {
+ return "";
+ }
+ const separator = rule.separator ?? "";
+ return rule.parts.map(buildPartExample).join(separator) + ".ext";
+}
+
+export { PART_TYPES, matchName, matchPart, buildExample, buildPartExample, stripExtension };
diff --git a/src/views/project-board/project-files/ProjectFiles.vue b/src/views/project-board/project-files/ProjectFiles.vue
index d1309c30b..386256887 100644
--- a/src/views/project-board/project-files/ProjectFiles.vue
+++ b/src/views/project-board/project-files/ProjectFiles.vue
@@ -26,6 +26,23 @@
+
+
+
+
+ {{ $t("NamingConstraint.managerTitle") }}
+
+
@@ -57,6 +74,8 @@ 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: {
@@ -71,6 +90,7 @@ export default {
const { currentProject } = useProjects();
const { loadProjectModels } = useModels();
const { projectFileStructure, loadProjectFileStructure } = useFiles();
+ const { openSidePanel } = useAppSidePanel();
const reloadData = debounce(async () => {
await Promise.all([
@@ -80,6 +100,15 @@ export default {
]);
}, 1000);
+ const openNamingConstraintsManager = () => {
+ openSidePanel("right", {
+ component: NamingConstraintsManager,
+ props: {
+ project: currentProject.value
+ }
+ });
+ };
+
return {
// References
fileStructure: projectFileStructure,
@@ -89,6 +118,7 @@ export default {
// Methods
isProjectAdmin,
reloadData,
+ openNamingConstraintsManager,
// Responsive breakpoints
...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");
+ });
+});