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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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"); + }); +});