diff --git a/bun.lock b/bun.lock index 59f550b20..c43973d88 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,6 @@ "": { "name": "@backoffice/source", "dependencies": { - "@angular/animations": "22.2.3", "@angular/common": "22.2.3", "@angular/compiler": "22.2.3", "@angular/core": "22.2.3", @@ -30,10 +29,8 @@ "@angular/compiler-cli": "22.2.3", "@angular/language-service": "22.2.3", "@angular/material": "22.2.4", - "@angular/pwa": "22.2.3", "@eslint/js": "^9.8.0", "@fontsource-variable/roboto": "^5.3.0", - "@ngneat/spectator": "^22.1.0", "@nx/angular": "23.3.0", "@nx/devkit": "23.3.0", "@nx/eslint": "23.3.0", @@ -53,26 +50,24 @@ "@swc/core": "~1.5.7", "@swc/helpers": "~0.5.11", "@tailwindcss/postcss": "^4.1.17", + "@types/js-yaml": "^4.0.9", "@types/node": "20.19.9", "@typescript-eslint/utils": "8.65.0", "@vitest/coverage-v8": "^4.0.8", "@vitest/ui": "^4.0.8", - "@xterm/xterm": "^5.5.0", "angular-eslint": "22.6.0", - "autoprefixer": "^10.4.16", "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0", "eslint": "^9.8.0", "eslint-config-prettier": "^10.0.0", "eslint-plugin-playwright": "^1.6.2", "firacode": "^6.2.0", "git-describe": "^4.1.1", "jiti": "2.4.2", + "js-yaml": "^4.1.1", "jsdom": "~22.1.0", "marked": "^17.0.0", "material-symbols": "^0.39.3", "monaco-editor": "^0.54.0", - "mqtt": "^5.14.1", "ng-mocks": "^14.14.0", "nx": "23.3.0", "postcss-import": "^16.1.1", @@ -81,16 +76,13 @@ "prettier": "^3.6.2", "prettier-plugin-organize-imports": "^4.3.0", "prettier-plugin-tailwindcss": "^0.7.1", - "qr": "^0.5.2", "rxjs": "^7.8.2", - "suneditor": "^2.47.8", "ts-md5": "^2.0.1", "tslib": "^2.3.0", "typescript": "~6.0.3", "typescript-eslint": "8.65.0", "vite": "8.3.5", "vitest": "^4.0.8", - "yaml": "^2.8.1", }, }, }, @@ -123,8 +115,6 @@ "@angular-eslint/utils": ["@angular-eslint/utils@22.6.0", "", { "dependencies": { "@angular-eslint/bundled-angular-compiler": "22.6.0" }, "peerDependencies": { "@typescript-eslint/utils": "^8.0.0", "eslint": "^9.0.0 || ^10.0.0", "typescript": "*" } }, "sha512-Nd5pZGgkBTvcCNMBt88iG6r/Tzk1XTWHpQQccwYUWYhz4PMim0yyXAEvqAabxag8S9iLPUEufpaOw20OOxNJ3w=="], - "@angular/animations": ["@angular/animations@22.2.3", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/core": "22.2.3" } }, "sha512-sslKnTrKwHwtTvT/pWj7UoYq0cfuX3PijZM7v0JtCajQkK1Nj0Ljob0mebxXBXUVBrOixfCnfBS0EG4JNEznFA=="], - "@angular/build": ["@angular/build@22.2.3", "", { "dependencies": { "@ampproject/remapping": "2.3.0", "@angular-devkit/architect": "0.2202.3", "@babel/core": "8.0.5", "@inquirer/confirm": "6.3.2", "@parcel/watcher": "2.6.0", "@vitejs/plugin-basic-ssl": "2.3.0", "beasties": "0.5.4", "browserslist": "^4.26.0", "chokidar": "5.0.0", "esbuild": "0.28.2", "https-proxy-agent": "9.1.0", "jsonc-parser": "3.3.1", "listr2": "11.1.0", "magic-string": "1.4.1", "mrmime": "2.0.1", "oxc-parser": "0.150.0", "parse5-html-rewriting-stream": "8.0.1", "picomatch": "4.0.7", "piscina": "5.3.2", "rolldown": "1.2.8", "sass": "1.104.1", "sass-embedded": "1.104.1", "semver": "7.8.5", "source-map-support": "0.5.21", "tinyglobby": "0.2.17", "vite": "8.3.0", "xxhash-wasm": "1.1.0" }, "optionalDependencies": { "lmdb": "3.5.6" }, "peerDependencies": { "@angular/compiler": "^22.0.0", "@angular/compiler-cli": "^22.0.0", "@angular/core": "^22.0.0", "@angular/localize": "^22.0.0", "@angular/platform-browser": "^22.0.0", "@angular/platform-server": "^22.0.0", "@angular/service-worker": "^22.0.0", "@angular/ssr": "^22.2.3", "istanbul-lib-instrument": "^6.0.0", "karma": "^6.4.0", "less": "^4.2.0", "ng-packagr": "^22.0.0", "postcss": "^8.4.0", "rollup": "^4.0.0", "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", "tslib": "^2.3.0", "typescript": ">=6.0 <6.1", "vitest": "^4.0.8 || ^5.0.0" }, "optionalPeers": ["@angular/core", "@angular/localize", "@angular/platform-browser", "@angular/platform-server", "@angular/service-worker", "@angular/ssr", "istanbul-lib-instrument", "karma", "less", "ng-packagr", "postcss", "rollup", "tailwindcss", "vitest"] }, "sha512-EtNEuPBUCyIuzdXrQ/nINvgFzW8bUI5AkAZtJiSn7nsFOlvaygaKN0kENNTO3zRl3T9B4GDog+Ks5Nec3FBkhg=="], "@angular/cdk": ["@angular/cdk@22.2.4", "", { "dependencies": { "parse5": "^8.0.0", "tslib": "^2.3.0" }, "peerDependencies": { "@angular/common": "^22.0.0 || ^23.0.0", "@angular/core": "^22.0.0 || ^23.0.0", "@angular/forms": "^22.0.0 || ^23.0.0", "@angular/platform-browser": "^22.0.0 || ^23.0.0", "rxjs": "^6.5.3 || ^7.4.0" } }, "sha512-c/DSWyCnN17f/EIl9Y1zVmSEWCAEdp4yeS53WWiHghIPg3kDX32Qbe/9b0/hlYfxcBSdPxtcgZMLSvqtu6hvkA=="], @@ -147,8 +137,6 @@ "@angular/platform-browser": ["@angular/platform-browser@22.2.3", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/animations": "22.2.3", "@angular/common": "22.2.3", "@angular/core": "22.2.3" }, "optionalPeers": ["@angular/animations"] }, "sha512-eWZGsbG4bSrSg9UB/NE8wORGVrh1u+x6rirPGpMWqq/mTi6q+nS+pnPxAxmiEs8wmPH5nCz5b2pRKQeYUkhAGA=="], - "@angular/pwa": ["@angular/pwa@22.2.3", "", { "dependencies": { "@angular-devkit/schematics": "22.2.3", "@schematics/angular": "22.2.3", "parse5-html-rewriting-stream": "8.0.1" }, "peerDependencies": { "@angular/cli": "^22.2.3" }, "optionalPeers": ["@angular/cli"] }, "sha512-ORyFtRHLRnLTWhOWY/Q+mVdCAorKEYVwi/USs6qMT+tJ4AQbNmxJ7klF337Uh9l/Tc1Tknjf3dM63uv1iGP64w=="], - "@angular/router": ["@angular/router@22.2.3", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/common": "22.2.3", "@angular/core": "22.2.3", "@angular/platform-browser": "22.2.3", "rxjs": "^6.5.3 || ^7.4.0" } }, "sha512-h4C3iJ3OXM9HItErmTo9fFrFRDj1DSqbvweHAWl8eYJvyuhDD80gBNgdpN8/5gpt9K1HV+PXk0YbyjyQ0zMfPA=="], "@angular/service-worker": ["@angular/service-worker@22.2.3", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/core": "22.2.3", "rxjs": "^6.5.3 || ^7.4.0" }, "bin": { "ngsw-config": "ngsw-config.js" } }, "sha512-saMdoMws2M9nIGfM2hdhCHhPospGq1UC1xR3V8pcPQWW9EgQqkpjLZODPrXxJjzxpvprnuHY7w/OSt7dqfbbtA=="], @@ -651,8 +639,6 @@ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.4", "", { "dependencies": { "@emnapi/core": "^1.1.0", "@emnapi/runtime": "^1.1.0", "@tybys/wasm-util": "^0.9.0" } }, "sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ=="], - "@ngneat/spectator": ["@ngneat/spectator@22.1.0", "", { "dependencies": { "@testing-library/dom": "^10.4.1", "jquery": "^3.7.1", "tslib": "^2.6.2" }, "peerDependencies": { "@angular/animations": ">= 20.0.0", "@angular/common": ">= 20.0.0", "@angular/router": ">= 20.0.0" } }, "sha512-5UxMxiSp8LeWCdAtMktd2NVPJhbm88nBbNoOlmBty7b4paHAFwEhr7NNSdNlnFH2ZF/iAE5fLqzkUAgCGSGmDA=="], - "@nx/angular": ["@nx/angular@23.3.0", "", { "dependencies": { "@nx/devkit": "23.3.0", "@nx/eslint": "23.3.0", "@nx/js": "23.3.0", "@nx/web": "23.3.0", "@phenomnomnominal/tsquery": "~6.2.0", "@typescript-eslint/type-utils": "^8.0.0", "jsonc-parser": "^3.2.0", "magic-string": "~0.30.2", "picocolors": "^1.1.0", "picomatch": "4.0.4", "semver": "^7.6.3", "tslib": "^2.3.0" }, "peerDependencies": { "@angular-devkit/build-angular": ">= 20.0.0 < 23.0.0", "@angular-devkit/core": ">= 20.0.0 < 23.0.0", "@angular-devkit/schematics": ">= 20.0.0 < 23.0.0", "@angular/build": ">= 20.0.0 < 23.0.0", "@nx/cypress": "23.3.0", "@nx/module-federation": "23.3.0", "@nx/playwright": "23.3.0", "@nx/rspack": "23.3.0", "@nx/webpack": "23.3.0", "@schematics/angular": ">= 20.0.0 < 23.0.0", "ng-packagr": ">= 20.0.0 < 23.0.0", "rxjs": "^6.5.3 || ^7.5.0", "webpack-merge": "^5.8.0" }, "optionalPeers": ["@angular-devkit/build-angular", "@angular/build", "@nx/cypress", "@nx/module-federation", "@nx/playwright", "@nx/rspack", "@nx/webpack", "ng-packagr", "webpack-merge"] }, "sha512-rRDHaJODyX0WXD5NCLmz21MihIhiUPg6LlSKDdTiLOCIAz7vLaJypQJeRCZUkgOdfFZkCvZKW/K95r67gUbLIw=="], "@nx/devkit": ["@nx/devkit@23.3.0", "", { "dependencies": { "ejs": "5.0.1", "minimatch": "10.2.5", "semver": "^7.6.3", "tslib": "^2.3.0", "yargs-parser": "21.1.1" }, "peerDependencies": { "nx": ">= 22 <= 24 || ^23.0.0-0" } }, "sha512-xGSV4gCwQsVdt49VRcfxd2L31dAO/9t9Isv2bM5O89vyEzwd/tkW/IQwUzWn3OdhCnHayd37S1tDyJ5DilCv7w=="], @@ -965,14 +951,10 @@ "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.17", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.17", "@tailwindcss/oxide": "4.1.17", "postcss": "^8.4.41", "tailwindcss": "4.1.17" } }, "sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw=="], - "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], - "@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="], - "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], - "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], @@ -987,6 +969,8 @@ "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], + "@types/jsesc": ["@types/jsesc@2.5.1", "", {}, "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], @@ -997,14 +981,10 @@ "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], - "@types/readable-stream": ["@types/readable-stream@4.0.22", "", { "dependencies": { "@types/node": "*" } }, "sha512-/FFhJpfCLAPwAcN3mFycNUa77ddnr8jTgF5VmSNetaemWB2cIlfCA9t0YTM3JAT0wOcv8D4tjPo7pkDhK3EJIg=="], - "@types/semver": ["@types/semver@7.5.8", "", {}, "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.65.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.65.0", "@typescript-eslint/type-utils": "8.65.0", "@typescript-eslint/utils": "8.65.0", "@typescript-eslint/visitor-keys": "8.65.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.65.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.65.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.65.0", "@typescript-eslint/types": "8.65.0", "@typescript-eslint/typescript-estree": "8.65.0", "@typescript-eslint/visitor-keys": "8.65.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA=="], @@ -1047,16 +1027,12 @@ "@vitest/utils": ["@vitest/utils@4.1.8", "", { "dependencies": { "@vitest/pretty-format": "4.1.8", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg=="], - "@xterm/xterm": ["@xterm/xterm@5.5.0", "", {}, "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="], - "@yarnpkg/lockfile": ["@yarnpkg/lockfile@1.1.0", "", {}, "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ=="], "@zkochan/js-yaml": ["@zkochan/js-yaml@0.0.7", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ=="], "abab": ["abab@2.0.6", "", {}, "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA=="], - "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], - "acorn": ["acorn@8.15.0", "", { "bin": "bin/acorn" }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], @@ -1079,7 +1055,7 @@ "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], @@ -1121,8 +1097,6 @@ "brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="], - "broker-factory": ["broker-factory@3.1.10", "", { "dependencies": { "@babel/runtime": "^7.28.4", "fast-unique-numbers": "^9.0.24", "tslib": "^2.8.1", "worker-factory": "^7.0.46" } }, "sha512-BzqK5GYFhvVFvO13uzPN0SCiOsOQuhMUbsGvTXDJMA2/N4GvIlFdxEuueE+60Zk841bBU5G3+fl2cqYEo0wgGg=="], - "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], @@ -1141,7 +1115,7 @@ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - "caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "caniuse-lite": ["caniuse-lite@1.0.30001793", "", {}, "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], @@ -1185,12 +1159,8 @@ "commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], - "commist": ["commist@3.2.0", "", {}, "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw=="], - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - "concat-stream": ["concat-stream@2.0.0", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A=="], - "confusing-browser-globals": ["confusing-browser-globals@1.0.11", "", {}, "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA=="], "convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], @@ -1229,8 +1199,6 @@ "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], - "date-fns-tz": ["date-fns-tz@3.2.0", "", { "peerDependencies": { "date-fns": "^3.0.0 || ^4.0.0" } }, "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ=="], - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], @@ -1255,8 +1223,6 @@ "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], - "dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], - "dom-serializer": ["dom-serializer@3.1.1", "", { "dependencies": { "domelementtype": "^3.0.0", "domhandler": "^6.0.0", "entities": "^8.0.0" } }, "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw=="], "domelementtype": ["domelementtype@3.0.0", "", {}, "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg=="], @@ -1343,12 +1309,8 @@ "event-emitter": ["event-emitter@0.3.5", "", { "dependencies": { "d": "1", "es5-ext": "~0.10.14" } }, "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA=="], - "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], - "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], - "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], - "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], "ext": ["ext@1.7.0", "", { "dependencies": { "type": "^2.7.2" } }, "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw=="], @@ -1365,8 +1327,6 @@ "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], - "fast-unique-numbers": ["fast-unique-numbers@9.0.24", "", { "dependencies": { "@babel/runtime": "^7.28.4", "tslib": "^2.8.1" } }, "sha512-Dv0BYn4waOWse94j16rsZ5w/0zoaCa74O3q6IZjMqaXbtT92Q+Sb6pPk+phGzD8Xh+nueQmSRI3tSCaHKidzKw=="], - "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], "fast-wrap-ansi": ["fast-wrap-ansi@0.2.0", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w=="], @@ -1439,8 +1399,6 @@ "he": ["he@1.2.0", "", { "bin": "bin/he" }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], - "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], - "hosted-git-info": ["hosted-git-info@10.1.1", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-DeOnSPAvOndYKfw075gt8yZzQ7S2hNztw34zBTfhIzLhmBTswIBg5/y+pqu/VD5cYWm5goAFTusDmUEmKZ0PEQ=="], "html-encoding-sniffer": ["html-encoding-sniffer@3.0.0", "", { "dependencies": { "whatwg-encoding": "^2.0.0" } }, "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA=="], @@ -1477,8 +1435,6 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], - "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], @@ -1515,10 +1471,6 @@ "jiti": ["jiti@2.4.2", "", { "bin": "lib/jiti-cli.mjs" }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], - "jquery": ["jquery@3.7.1", "", {}, "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg=="], - - "js-sdsl": ["js-sdsl@4.3.0", "", {}, "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ=="], - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": "bin/js-yaml.js" }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], @@ -1589,9 +1541,7 @@ "log-update": ["log-update@8.0.0", "", { "dependencies": { "ansi-escapes": "^7.3.0", "cli-cursor": "^5.0.0", "slice-ansi": "^9.0.0", "string-width": "^8.2.0", "strip-ansi": "^7.2.0", "wrap-ansi": "^10.0.0" } }, "sha512-lddSgOt3bPASrylL54ZSpy8nBHns+vBVSoILlVOx+dei300pnLRN958rj/EdlVLKuWlSESU3qdnDZdAI7FXYGg=="], - "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - - "lz-string": ["lz-string@1.5.0", "", { "bin": "bin/bin.js" }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], + "lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -1633,10 +1583,6 @@ "monaco-editor": ["monaco-editor@0.54.0", "", { "dependencies": { "dompurify": "3.1.7", "marked": "14.0.0" } }, "sha512-hx45SEUoLatgWxHKCmlLJH81xBo0uXP4sRkESUpmDQevfi+e7K1VuiSprK6UpQ8u4zOcKNiH0pMvHvlMWA/4cw=="], - "mqtt": ["mqtt@5.14.1", "", { "dependencies": { "@types/readable-stream": "^4.0.21", "@types/ws": "^8.18.1", "commist": "^3.2.0", "concat-stream": "^2.0.0", "debug": "^4.4.1", "help-me": "^5.0.0", "lru-cache": "^10.4.3", "minimist": "^1.2.8", "mqtt-packet": "^9.0.2", "number-allocator": "^1.0.14", "readable-stream": "^4.7.0", "rfdc": "^1.4.1", "socks": "^2.8.6", "split2": "^4.2.0", "worker-timers": "^8.0.23", "ws": "^8.18.3" }, "bin": { "mqtt": "build/bin/mqtt.js", "mqtt_pub": "build/bin/pub.js", "mqtt_sub": "build/bin/sub.js" } }, "sha512-NxkPxE70Uq3Ph7goefQa7ggSsVzHrayCD0OyxlJgITN/EbzlZN+JEPmaAZdxP1LsIT5FamDyILoQTF72W7Nnbw=="], - - "mqtt-packet": ["mqtt-packet@9.0.2", "", { "dependencies": { "bl": "^6.0.8", "debug": "^4.3.4", "process-nextick-args": "^2.0.1" } }, "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA=="], - "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -1677,8 +1623,6 @@ "nth-check": ["nth-check@3.0.1", "", { "dependencies": { "boolbase": "^2.0.0" } }, "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ=="], - "number-allocator": ["number-allocator@1.0.14", "", { "dependencies": { "debug": "^4.3.1", "js-sdsl": "4.3.0" } }, "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA=="], - "nwsapi": ["nwsapi@2.2.22", "", {}, "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ=="], "nx": ["nx@23.3.0", "", { "dependencies": { "@clack/core": "1.4.3", "@clack/prompts": "1.7.0", "@emnapi/core": "1.4.5", "@emnapi/runtime": "1.4.5", "@emnapi/wasi-threads": "1.0.4", "@jest/diff-sequences": "30.0.1", "@napi-rs/wasm-runtime": "0.2.4", "@tybys/wasm-util": "0.9.0", "@yarnpkg/lockfile": "1.1.0", "@zkochan/js-yaml": "0.0.7", "ansi-regex": "5.0.1", "ansi-styles": "4.3.0", "argparse": "2.0.1", "balanced-match": "4.0.3", "base64-js": "1.5.1", "bl": "4.1.0", "brace-expansion": "5.0.12", "buffer": "5.7.1", "bundle-name": "4.1.0", "chalk": "4.1.2", "cli-cursor": "3.1.0", "cli-spinners": "2.6.1", "cliui": "8.0.1", "clone": "1.0.4", "color-convert": "2.0.1", "color-name": "1.1.4", "default-browser": "5.2.1", "default-browser-id": "5.0.0", "defaults": "1.0.4", "define-lazy-prop": "3.0.0", "dotenv": "16.4.7", "dotenv-expand": "12.0.3", "ejs": "5.0.1", "emoji-regex": "8.0.0", "end-of-stream": "1.4.5", "escalade": "3.2.0", "escape-string-regexp": "1.0.5", "fast-string-truncated-width": "3.0.3", "fast-string-width": "3.0.2", "fast-wrap-ansi": "0.2.0", "figures": "3.2.0", "fs-constants": "1.0.0", "get-caller-file": "2.0.5", "has-flag": "4.0.0", "ieee754": "1.2.1", "ignore": "7.0.6", "inherits": "2.0.4", "is-docker": "3.0.0", "is-fullwidth-code-point": "3.0.0", "is-inside-container": "1.0.0", "is-interactive": "1.0.0", "is-unicode-supported": "0.1.0", "is-wsl": "3.1.0", "isexe": "2.0.0", "json5": "2.2.3", "jsonc-parser": "3.3.1", "lines-and-columns": "2.0.3", "log-symbols": "4.1.0", "mimic-fn": "2.1.0", "minimatch": "10.2.5", "minimist": "1.2.8", "npm-run-path": "4.0.1", "once": "1.4.0", "onetime": "5.1.2", "open": "10.1.0", "ora": "5.4.1", "path-key": "3.1.1", "picocolors": "1.1.1", "readable-stream": "3.6.2", "require-directory": "2.1.1", "resolve.exports": "2.0.3", "restore-cursor": "3.1.0", "run-applescript": "7.0.0", "safe-buffer": "5.2.1", "semver": "7.8.5", "signal-exit": "3.0.7", "sisteransi": "1.0.5", "smol-toml": "1.9.0", "string-width": "4.2.3", "string_decoder": "1.3.0", "strip-ansi": "6.0.1", "strip-bom": "3.0.0", "supports-color": "7.2.0", "tar-stream": "2.2.0", "tmp": "0.2.7", "tsconfig-paths": "4.2.0", "tslib": "2.8.1", "undici": "7.29.0", "util-deprecate": "1.0.2", "wcwidth": "1.0.1", "which": "3.0.1", "wrap-ansi": "7.0.0", "wrappy": "1.0.2", "y18n": "5.0.8", "yaml": "2.9.0", "yargs": "17.7.2", "yargs-parser": "21.1.1" }, "optionalDependencies": { "@nx/nx-darwin-arm64": "23.3.0", "@nx/nx-darwin-x64": "23.3.0", "@nx/nx-freebsd-x64": "23.3.0", "@nx/nx-linux-arm-gnueabihf": "23.3.0", "@nx/nx-linux-arm64-gnu": "23.3.0", "@nx/nx-linux-arm64-musl": "23.3.0", "@nx/nx-linux-x64-gnu": "23.3.0", "@nx/nx-linux-x64-musl": "23.3.0", "@nx/nx-win32-arm64-msvc": "23.3.0", "@nx/nx-win32-x64-msvc": "23.3.0" }, "peerDependencies": { "@swc-node/register": "^1.11.1", "@swc/core": "^1.15.8" }, "optionalPeers": ["@swc-node/register", "@swc/core"], "bin": { "nx": "./dist/bin/nx.js", "nx-cloud": "./dist/bin/nx-cloud.js" } }, "sha512-R7Kl+hURSGiG9uEjc19Cg3o+vGCDHqCVp6mioiBMEsOSol/LgzP+K8TVF06TnnSC8uzxMeB0ymu+LGMJGXnbbw=="], @@ -1823,14 +1767,8 @@ "prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.7.1", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-sort-imports", "prettier-plugin-svelte"] }, "sha512-Bzv1LZcuiR1Sk02iJTS1QzlFNp/o5l2p3xkopwOrbPmtMeh3fK9rVW5M3neBQzHq+kGKj/4LGQMTNcTH4NGPtQ=="], - "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], - "proc-log": ["proc-log@7.0.0", "", {}, "sha512-FYgfaA69XZ93zaXLoMNQ+ViDXGGBgR8aLh03txzcFhV+9xOXx7+8DLCULrKKpR9+GsH9ZfHm82aSUPpozX0Ztg=="], - "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], - - "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], - "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], @@ -1845,17 +1783,13 @@ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "qr": ["qr@0.5.2", "", {}, "sha512-91M3sVlA7xCFpkJtYX5xzVH8tDo4rNZ7jr8v+1CRgPVkZ4D+Vl9y8rtZWJ/YkEUM6U/h0FAu5W/JAK7iowOteA=="], - "qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], "querystringify": ["querystringify@2.2.0", "", {}, "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="], - "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], - "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], - "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], @@ -1891,8 +1825,6 @@ "restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], - "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], - "rolldown": ["rolldown@1.2.8", "", { "dependencies": { "@oxc-project/types": "=0.149.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.8", "@rolldown/binding-android-arm64": "1.2.8", "@rolldown/binding-darwin-arm64": "1.2.8", "@rolldown/binding-darwin-x64": "1.2.8", "@rolldown/binding-freebsd-x64": "1.2.8", "@rolldown/binding-linux-arm-gnueabihf": "1.2.8", "@rolldown/binding-linux-arm64-gnu": "1.2.8", "@rolldown/binding-linux-arm64-musl": "1.2.8", "@rolldown/binding-linux-ppc64-gnu": "1.2.8", "@rolldown/binding-linux-s390x-gnu": "1.2.8", "@rolldown/binding-linux-x64-gnu": "1.2.8", "@rolldown/binding-linux-x64-musl": "1.2.8", "@rolldown/binding-openharmony-arm64": "1.2.8", "@rolldown/binding-win32-arm64-msvc": "1.2.8", "@rolldown/binding-win32-x64-msvc": "1.2.8" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ=="], "rollup": ["rollup@4.52.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.3", "@rollup/rollup-android-arm64": "4.52.3", "@rollup/rollup-darwin-arm64": "4.52.3", "@rollup/rollup-darwin-x64": "4.52.3", "@rollup/rollup-freebsd-arm64": "4.52.3", "@rollup/rollup-freebsd-x64": "4.52.3", "@rollup/rollup-linux-arm-gnueabihf": "4.52.3", "@rollup/rollup-linux-arm-musleabihf": "4.52.3", "@rollup/rollup-linux-arm64-gnu": "4.52.3", "@rollup/rollup-linux-arm64-musl": "4.52.3", "@rollup/rollup-linux-loong64-gnu": "4.52.3", "@rollup/rollup-linux-ppc64-gnu": "4.52.3", "@rollup/rollup-linux-riscv64-gnu": "4.52.3", "@rollup/rollup-linux-riscv64-musl": "4.52.3", "@rollup/rollup-linux-s390x-gnu": "4.52.3", "@rollup/rollup-linux-x64-gnu": "4.52.3", "@rollup/rollup-linux-x64-musl": "4.52.3", "@rollup/rollup-openharmony-arm64": "4.52.3", "@rollup/rollup-win32-arm64-msvc": "4.52.3", "@rollup/rollup-win32-ia32-msvc": "4.52.3", "@rollup/rollup-win32-x64-gnu": "4.52.3", "@rollup/rollup-win32-x64-msvc": "4.52.3", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A=="], @@ -1979,12 +1911,8 @@ "slice-ansi": ["slice-ansi@9.0.1", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-aBY19bn/XA+hKOKX0qL7Z0UfdBsbQvc9hn993U8ALUjzxCvDcuZqLoRXjGJrUARWAlwMnsRVD9sw2AFNOYvalA=="], - "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], - "smol-toml": ["smol-toml@1.9.0", "", {}, "sha512-hpd+HLON7HdZXqYchMM/+LaTTbdK0AU3NngIJ4KVyWbY9bfQqdL9cD+4yf6dUoU2Ap4VsU0JkQi6FxAI1B2mXQ=="], - "socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="], - "source-map": ["source-map@0.8.0", "", {}, "sha512-d8EqvL+k/SOXCreS/SUzg2ciyHqBBLcN/yuRjFsbvVhHTE2pgei7oAhmPM7kWFbkX6OSMQfUq4KbkF3au9lhYQ=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -1993,8 +1921,6 @@ "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], - "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], - "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], "std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], @@ -2013,8 +1939,6 @@ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - "suneditor": ["suneditor@2.47.8", "", {}, "sha512-GGzaN1m+rFIR/zHdRxSH5JvzzJIF4UoaX2bWmcvk6NIcq2qgvdkR0iO+yFyjIL6zTVMeBI/GWkJMvOh8Qe5ehw=="], - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], @@ -2065,8 +1989,6 @@ "type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], - "typedarray": ["typedarray@0.0.6", "", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="], - "typedarray-to-buffer": ["typedarray-to-buffer@3.1.5", "", { "dependencies": { "is-typedarray": "^1.0.0" } }, "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q=="], "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], @@ -2151,14 +2073,6 @@ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - "worker-factory": ["worker-factory@7.0.46", "", { "dependencies": { "@babel/runtime": "^7.28.4", "fast-unique-numbers": "^9.0.24", "tslib": "^2.8.1" } }, "sha512-Sr1hq2FMgNa04UVhYQacsw+i58BtMimzDb4+CqYphZ97OfefRpURu0UZ+JxMr/H36VVJBfuVkxTK7MytsanC3w=="], - - "worker-timers": ["worker-timers@8.0.25", "", { "dependencies": { "@babel/runtime": "^7.28.4", "tslib": "^2.8.1", "worker-timers-broker": "^8.0.11", "worker-timers-worker": "^9.0.11" } }, "sha512-X7Z5dmM6PlrEnaadtFQOyXHGD/IysPA3HZzaC2koqsU1VI+RvyGmjiiLiUBQixK8PH5R7ilkOzZupWskNRaXmA=="], - - "worker-timers-broker": ["worker-timers-broker@8.0.11", "", { "dependencies": { "@babel/runtime": "^7.28.4", "broker-factory": "^3.1.10", "fast-unique-numbers": "^9.0.24", "tslib": "^2.8.1", "worker-timers-worker": "^9.0.11" } }, "sha512-uwhxKru8BI9m2tsogxr2fB6POZ8LB2xH+Pu3R0mvQnAZLPgLD6K3IX4LNKPTEgTJ/j5VsuQPB+gLI1NBNKkPlg=="], - - "worker-timers-worker": ["worker-timers-worker@9.0.11", "", { "dependencies": { "@babel/runtime": "^7.28.4", "tslib": "^2.8.1", "worker-factory": "^7.0.46" } }, "sha512-pArb5xtgHWImYpXhjg1OFv7JFG0ubmccb73TFoXHXjG830fFj+16N57q9YeBnZX52dn+itRrMoJZ9HaZBVzDaA=="], - "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], @@ -2177,7 +2091,7 @@ "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "yaml": ["yaml@2.8.1", "", { "bin": "bin.mjs" }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="], + "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], "yargs": ["yargs@18.1.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^8.2.1", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg=="], @@ -2197,8 +2111,6 @@ "@angular-eslint/builder/@angular-devkit/architect": ["@angular-devkit/architect@0.2201.9", "", { "dependencies": { "@angular-devkit/core": "22.1.9", "rxjs": "7.8.2" }, "bin": { "architect": "bin/cli.js" } }, "sha512-earmt8lj9b/PadJJBTFEd+DMzZKbzaFDdXrTO2zdfmTViuo9OeRkupGQtZkH8KVPMGf9JwNoAvHSAr7k0i3oUA=="], - "@angular-eslint/eslint-plugin-template/aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], - "@angular-eslint/schematics/ignore": ["ignore@7.0.12", "", {}, "sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg=="], "@angular-eslint/template-parser/eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="], @@ -2543,8 +2455,6 @@ "@tailwindcss/node/lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], - "@testing-library/dom/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], - "@typescript-eslint/typescript-estree/semver": ["semver@7.8.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA=="], "@typescript-eslint/typescript-estree/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], @@ -2561,6 +2471,8 @@ "autoprefixer/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], + "autoprefixer/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "babel-plugin-const-enum/@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], "babel-plugin-const-enum/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], @@ -2577,16 +2489,10 @@ "beasties/postcss": ["postcss@8.5.31", "", { "dependencies": { "nanoid": "^3.3.20", "picocolors": "^1.1.1", "source-map-js": "^1.2.2" } }, "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg=="], - "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - - "browserslist/caniuse-lite": ["caniuse-lite@1.0.30001793", "", {}, "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA=="], - "bundle-name/run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], "cli-truncate/string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], - "concat-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "core-js-compat/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], "cosmiconfig/yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], @@ -2609,8 +2515,6 @@ "git-describe/semver": ["semver@5.7.2", "", { "bin": "bin/semver" }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], - "hosted-git-info/lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="], - "http-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], "istanbul-lib-report/make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], @@ -2651,24 +2555,16 @@ "monaco-editor/marked": ["marked@14.0.0", "", { "bin": "bin/marked.js" }, "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ=="], - "mqtt/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" } }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], - - "mqtt-packet/bl": ["bl@6.1.4", "", { "dependencies": { "@types/readable-stream": "^4.0.0", "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^4.2.0" } }, "sha512-ZV/9asSuknOExbM/zPPA8z00lc1ihPKWaStHkkQrxHNeYx+yY+TmF+v80dpv2G0mv3HVXBu7ryoAsxbFFhf4eg=="], - "nx/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], "nx/ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="], "nx/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], - "nx/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "nx/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="], "nx/which": ["which@3.0.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg=="], - "nx/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], - "nx/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], "open/default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg=="], @@ -2697,10 +2593,6 @@ "postcss-preset-env/browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], - "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - - "readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], - "rolldown/@oxc-project/types": ["@oxc-project/types@0.149.0", "", {}, "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA=="], "sass/source-map-js": ["source-map-js@1.2.2", "", {}, "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw=="], @@ -2713,8 +2605,6 @@ "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "tinyglobby/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], "vite/postcss": ["postcss@8.5.31", "", { "dependencies": { "nanoid": "^3.3.20", "picocolors": "^1.1.1", "source-map-js": "^1.2.2" } }, "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg=="], @@ -4645,8 +4535,6 @@ "@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], - "@testing-library/dom/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - "@typescript-eslint/typescript-estree/tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], "@vitest/ui/tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], @@ -4757,6 +4645,8 @@ "core-js-compat/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "core-js-compat/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "core-js-compat/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "core-js-compat/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -4793,8 +4683,6 @@ "magicast/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - "mqtt-packet/bl/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], - "open/default-browser/default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], "ora/cli-cursor/restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], @@ -4805,6 +4693,8 @@ "postcss-preset-env/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "postcss-preset-env/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "postcss-preset-env/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "postcss-preset-env/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -4957,6 +4847,8 @@ "@babel/helper-define-polyfill-provider/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/helper-define-polyfill-provider/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/helper-define-polyfill-provider/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/helper-define-polyfill-provider/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -5387,6 +5279,8 @@ "@babel/plugin-transform-classes/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-classes/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-classes/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-classes/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -5589,6 +5483,8 @@ "@babel/plugin-transform-function-name/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-function-name/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-function-name/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-function-name/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -5865,6 +5761,8 @@ "@babel/plugin-transform-object-rest-spread/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-object-rest-spread/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-object-rest-spread/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-object-rest-spread/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6231,6 +6129,8 @@ "@babel/preset-env/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/preset-env/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/preset-env/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/preset-env/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6447,6 +6347,8 @@ "@babel/helper-create-class-features-plugin/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/helper-create-class-features-plugin/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/helper-create-class-features-plugin/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/helper-create-class-features-plugin/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6455,6 +6357,8 @@ "@babel/helper-create-regexp-features-plugin/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/helper-create-regexp-features-plugin/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/helper-create-regexp-features-plugin/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/helper-create-regexp-features-plugin/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6463,6 +6367,8 @@ "@babel/helper-define-polyfill-provider/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/helper-define-polyfill-provider/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/helper-define-polyfill-provider/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/helper-define-polyfill-provider/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6471,6 +6377,8 @@ "@babel/helper-module-transforms/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/helper-module-transforms/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/helper-module-transforms/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/helper-module-transforms/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6479,6 +6387,8 @@ "@babel/helper-remap-async-to-generator/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/helper-remap-async-to-generator/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/helper-remap-async-to-generator/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/helper-remap-async-to-generator/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6487,6 +6397,8 @@ "@babel/helper-replace-supers/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/helper-replace-supers/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/helper-replace-supers/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/helper-replace-supers/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6495,6 +6407,8 @@ "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6503,6 +6417,8 @@ "@babel/plugin-bugfix-safari-class-field-initializer-scope/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-bugfix-safari-class-field-initializer-scope/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-bugfix-safari-class-field-initializer-scope/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-bugfix-safari-class-field-initializer-scope/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6511,6 +6427,8 @@ "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6519,6 +6437,8 @@ "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6527,6 +6447,8 @@ "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6535,6 +6457,8 @@ "@babel/plugin-proposal-decorators/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-proposal-decorators/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-proposal-decorators/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-proposal-decorators/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6543,6 +6467,8 @@ "@babel/plugin-proposal-private-property-in-object/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-proposal-private-property-in-object/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-proposal-private-property-in-object/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-proposal-private-property-in-object/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6551,6 +6477,8 @@ "@babel/plugin-syntax-decorators/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-syntax-decorators/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-syntax-decorators/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-syntax-decorators/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6559,6 +6487,8 @@ "@babel/plugin-syntax-import-assertions/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-syntax-import-assertions/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-syntax-import-assertions/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-syntax-import-assertions/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6567,6 +6497,8 @@ "@babel/plugin-syntax-import-attributes/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-syntax-import-attributes/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-syntax-import-attributes/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-syntax-import-attributes/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6575,6 +6507,8 @@ "@babel/plugin-syntax-jsx/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-syntax-jsx/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-syntax-jsx/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-syntax-jsx/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6583,6 +6517,8 @@ "@babel/plugin-syntax-typescript/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-syntax-typescript/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-syntax-typescript/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-syntax-typescript/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6591,6 +6527,8 @@ "@babel/plugin-syntax-unicode-sets-regex/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-syntax-unicode-sets-regex/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-syntax-unicode-sets-regex/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-syntax-unicode-sets-regex/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6599,6 +6537,8 @@ "@babel/plugin-transform-arrow-functions/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-arrow-functions/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-arrow-functions/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-arrow-functions/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6607,6 +6547,8 @@ "@babel/plugin-transform-async-generator-functions/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-async-generator-functions/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-async-generator-functions/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-async-generator-functions/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6615,6 +6557,8 @@ "@babel/plugin-transform-async-to-generator/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-async-to-generator/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-async-to-generator/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-async-to-generator/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6623,6 +6567,8 @@ "@babel/plugin-transform-block-scoped-functions/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-block-scoped-functions/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-block-scoped-functions/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-block-scoped-functions/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6631,6 +6577,8 @@ "@babel/plugin-transform-block-scoping/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-block-scoping/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-block-scoping/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-block-scoping/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6639,6 +6587,8 @@ "@babel/plugin-transform-class-properties/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-class-properties/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-class-properties/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-class-properties/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6647,6 +6597,8 @@ "@babel/plugin-transform-class-static-block/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-class-static-block/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-class-static-block/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-class-static-block/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6655,6 +6607,8 @@ "@babel/plugin-transform-classes/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-classes/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-classes/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-classes/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6663,6 +6617,8 @@ "@babel/plugin-transform-computed-properties/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-computed-properties/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-computed-properties/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-computed-properties/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6671,6 +6627,8 @@ "@babel/plugin-transform-destructuring/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-destructuring/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-destructuring/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-destructuring/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6679,6 +6637,8 @@ "@babel/plugin-transform-dotall-regex/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-dotall-regex/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-dotall-regex/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-dotall-regex/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6687,6 +6647,8 @@ "@babel/plugin-transform-duplicate-keys/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-duplicate-keys/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-duplicate-keys/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-duplicate-keys/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6695,6 +6657,8 @@ "@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6703,6 +6667,8 @@ "@babel/plugin-transform-dynamic-import/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-dynamic-import/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-dynamic-import/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-dynamic-import/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6711,6 +6677,8 @@ "@babel/plugin-transform-explicit-resource-management/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-explicit-resource-management/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-explicit-resource-management/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-explicit-resource-management/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6719,6 +6687,8 @@ "@babel/plugin-transform-exponentiation-operator/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-exponentiation-operator/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-exponentiation-operator/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-exponentiation-operator/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6727,6 +6697,8 @@ "@babel/plugin-transform-export-namespace-from/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-export-namespace-from/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-export-namespace-from/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-export-namespace-from/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6735,6 +6707,8 @@ "@babel/plugin-transform-for-of/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-for-of/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-for-of/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-for-of/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6743,6 +6717,8 @@ "@babel/plugin-transform-function-name/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-function-name/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-function-name/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-function-name/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6751,6 +6727,8 @@ "@babel/plugin-transform-json-strings/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-json-strings/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-json-strings/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-json-strings/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6759,6 +6737,8 @@ "@babel/plugin-transform-literals/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-literals/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-literals/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-literals/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6767,6 +6747,8 @@ "@babel/plugin-transform-logical-assignment-operators/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-logical-assignment-operators/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-logical-assignment-operators/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-logical-assignment-operators/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6775,6 +6757,8 @@ "@babel/plugin-transform-member-expression-literals/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-member-expression-literals/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-member-expression-literals/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-member-expression-literals/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6783,6 +6767,8 @@ "@babel/plugin-transform-modules-amd/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-modules-amd/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-modules-amd/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-modules-amd/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6793,6 +6779,8 @@ "@babel/plugin-transform-modules-commonjs/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-modules-commonjs/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-modules-commonjs/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-modules-commonjs/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6803,6 +6791,8 @@ "@babel/plugin-transform-modules-systemjs/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-modules-systemjs/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-modules-systemjs/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-modules-systemjs/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6811,6 +6801,8 @@ "@babel/plugin-transform-modules-umd/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-modules-umd/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-modules-umd/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-modules-umd/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6821,6 +6813,8 @@ "@babel/plugin-transform-named-capturing-groups-regex/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-named-capturing-groups-regex/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-named-capturing-groups-regex/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-named-capturing-groups-regex/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6829,6 +6823,8 @@ "@babel/plugin-transform-new-target/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-new-target/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-new-target/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-new-target/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6837,6 +6833,8 @@ "@babel/plugin-transform-nullish-coalescing-operator/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-nullish-coalescing-operator/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-nullish-coalescing-operator/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-nullish-coalescing-operator/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6845,6 +6843,8 @@ "@babel/plugin-transform-numeric-separator/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-numeric-separator/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-numeric-separator/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-numeric-separator/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6853,6 +6853,8 @@ "@babel/plugin-transform-object-rest-spread/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-object-rest-spread/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-object-rest-spread/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-object-rest-spread/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6861,6 +6863,8 @@ "@babel/plugin-transform-object-super/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-object-super/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-object-super/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-object-super/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6869,6 +6873,8 @@ "@babel/plugin-transform-optional-catch-binding/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-optional-catch-binding/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-optional-catch-binding/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-optional-catch-binding/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6877,6 +6883,8 @@ "@babel/plugin-transform-optional-chaining/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-optional-chaining/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-optional-chaining/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-optional-chaining/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6885,6 +6893,8 @@ "@babel/plugin-transform-parameters/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-parameters/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-parameters/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-parameters/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6893,6 +6903,8 @@ "@babel/plugin-transform-private-methods/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-private-methods/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-private-methods/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-private-methods/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6901,6 +6913,8 @@ "@babel/plugin-transform-private-property-in-object/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-private-property-in-object/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-private-property-in-object/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-private-property-in-object/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6909,6 +6923,8 @@ "@babel/plugin-transform-property-literals/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-property-literals/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-property-literals/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-property-literals/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6917,6 +6933,8 @@ "@babel/plugin-transform-regenerator/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-regenerator/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-regenerator/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-regenerator/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6925,6 +6943,8 @@ "@babel/plugin-transform-regexp-modifiers/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-regexp-modifiers/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-regexp-modifiers/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-regexp-modifiers/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6933,6 +6953,8 @@ "@babel/plugin-transform-reserved-words/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-reserved-words/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-reserved-words/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-reserved-words/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6941,6 +6963,8 @@ "@babel/plugin-transform-runtime/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-runtime/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-runtime/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-runtime/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6949,6 +6973,8 @@ "@babel/plugin-transform-shorthand-properties/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-shorthand-properties/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-shorthand-properties/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-shorthand-properties/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6957,6 +6983,8 @@ "@babel/plugin-transform-spread/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-spread/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-spread/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-spread/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6965,6 +6993,8 @@ "@babel/plugin-transform-sticky-regex/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-sticky-regex/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-sticky-regex/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-sticky-regex/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6973,6 +7003,8 @@ "@babel/plugin-transform-template-literals/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-template-literals/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-template-literals/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-template-literals/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6981,6 +7013,8 @@ "@babel/plugin-transform-typeof-symbol/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-typeof-symbol/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-typeof-symbol/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-typeof-symbol/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6989,6 +7023,8 @@ "@babel/plugin-transform-typescript/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-typescript/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-typescript/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-typescript/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -6997,6 +7033,8 @@ "@babel/plugin-transform-unicode-escapes/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-unicode-escapes/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-unicode-escapes/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-unicode-escapes/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -7005,6 +7043,8 @@ "@babel/plugin-transform-unicode-property-regex/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-unicode-property-regex/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-unicode-property-regex/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-unicode-property-regex/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -7013,6 +7053,8 @@ "@babel/plugin-transform-unicode-regex/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-unicode-regex/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-unicode-regex/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-unicode-regex/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -7021,6 +7063,8 @@ "@babel/plugin-transform-unicode-sets-regex/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/plugin-transform-unicode-sets-regex/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/plugin-transform-unicode-sets-regex/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/plugin-transform-unicode-sets-regex/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -7029,6 +7073,8 @@ "@babel/preset-env/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/preset-env/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/preset-env/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/preset-env/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -7037,6 +7083,8 @@ "@babel/preset-modules/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/preset-modules/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/preset-modules/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/preset-modules/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -7045,6 +7093,8 @@ "@babel/preset-typescript/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@babel/preset-typescript/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@babel/preset-typescript/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@babel/preset-typescript/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -7053,6 +7103,8 @@ "@nx/js/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "@nx/js/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "@nx/js/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "@nx/js/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -7061,6 +7113,8 @@ "babel-plugin-const-enum/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "babel-plugin-const-enum/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "babel-plugin-const-enum/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "babel-plugin-const-enum/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -7069,6 +7123,8 @@ "babel-plugin-polyfill-corejs2/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "babel-plugin-polyfill-corejs2/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "babel-plugin-polyfill-corejs2/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "babel-plugin-polyfill-corejs2/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -7077,6 +7133,8 @@ "babel-plugin-polyfill-corejs3/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "babel-plugin-polyfill-corejs3/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "babel-plugin-polyfill-corejs3/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "babel-plugin-polyfill-corejs3/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -7085,6 +7143,8 @@ "babel-plugin-polyfill-regenerator/@babel/core/@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": "dist/cli.js" }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], + "babel-plugin-polyfill-regenerator/@babel/core/@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "babel-plugin-polyfill-regenerator/@babel/core/@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.256", "", {}, "sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ=="], "babel-plugin-polyfill-regenerator/@babel/core/@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], diff --git a/e2e/src/pages/admin.page.ts b/e2e/src/pages/admin.page.ts index 477708ee6..b06913e9f 100644 --- a/e2e/src/pages/admin.page.ts +++ b/e2e/src/pages/admin.page.ts @@ -54,7 +54,7 @@ export class AdminPage extends BasePage { */ async selectApiKeyDomain(domain = 'Place Technology'): Promise { await this.page - .locator('admin-api-keys mat-select[name="type"]') + .locator('admin-api-keys admin-domain-select mat-select') .click(); await this.page .locator('mat-option') diff --git a/package.json b/package.json index 404560dee..f2ec144a7 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ }, "private": true, "dependencies": { - "@angular/animations": "22.2.3", "@angular/common": "22.2.3", "@angular/compiler": "22.2.3", "@angular/core": "22.2.3", @@ -40,10 +39,8 @@ "@angular/compiler-cli": "22.2.3", "@angular/language-service": "22.2.3", "@angular/material": "22.2.4", - "@angular/pwa": "22.2.3", "@eslint/js": "^9.8.0", "@fontsource-variable/roboto": "^5.3.0", - "@ngneat/spectator": "^22.1.0", "@nx/angular": "23.3.0", "@nx/devkit": "23.3.0", "@nx/eslint": "23.3.0", @@ -63,26 +60,24 @@ "@swc/core": "~1.5.7", "@swc/helpers": "~0.5.11", "@tailwindcss/postcss": "^4.1.17", + "@types/js-yaml": "^4.0.9", "@types/node": "20.19.9", "@typescript-eslint/utils": "8.65.0", "@vitest/coverage-v8": "^4.0.8", "@vitest/ui": "^4.0.8", - "@xterm/xterm": "^5.5.0", "angular-eslint": "22.6.0", - "autoprefixer": "^10.4.16", "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0", "eslint": "^9.8.0", "eslint-config-prettier": "^10.0.0", "eslint-plugin-playwright": "^1.6.2", "firacode": "^6.2.0", "git-describe": "^4.1.1", "jiti": "2.4.2", + "js-yaml": "^4.1.1", "jsdom": "~22.1.0", "marked": "^17.0.0", "material-symbols": "^0.39.3", "monaco-editor": "^0.54.0", - "mqtt": "^5.14.1", "ng-mocks": "^14.14.0", "nx": "23.3.0", "postcss-import": "^16.1.1", @@ -91,16 +86,13 @@ "prettier": "^3.6.2", "prettier-plugin-organize-imports": "^4.3.0", "prettier-plugin-tailwindcss": "^0.7.1", - "qr": "^0.5.2", "rxjs": "^7.8.2", - "suneditor": "^2.47.8", "ts-md5": "^2.0.1", "tslib": "^2.3.0", "typescript": "~6.0.3", "typescript-eslint": "8.65.0", "vite": "8.3.5", - "vitest": "^4.0.8", - "yaml": "^2.8.1" + "vitest": "^4.0.8" }, "nx": { "includedScripts": [] diff --git a/public/assets/not-supported.html b/public/assets/not-supported.html deleted file mode 100644 index 0c4d9f4da..000000000 --- a/public/assets/not-supported.html +++ /dev/null @@ -1,156 +0,0 @@ - - - - - - - Browser not supported :O - - - - -
-
- -
-

- Oh snap! We don't support this version of your browser, and - neither should you! -

-

- You are visiting this page because we detected an unsupported - browser. Your browser does not support security features that we - require. We highly recommend that you update your browser. -

-

Recommended browsers:

-
-
-
- -
-
Chrome
- -
-
-
- -
-
Firefox
- -
- -
-
- -
-
Safari
- -
- -
-
- - diff --git a/src/app/admin/admin-data.service.ts b/src/app/admin/admin-data.service.ts index aa96d7098..7021c239f 100644 --- a/src/app/admin/admin-data.service.ts +++ b/src/app/admin/admin-data.service.ts @@ -1,24 +1,19 @@ import { Service, WritableSignal, signal } from '@angular/core'; import { PlaceDomain, authority, queryDomains } from '@placeos/ts-client'; - -export type AdminSortState = [string, boolean]; +import { TableSort } from '../ui/simple-table.component'; @Service() export class AdminDataService { private readonly _domain_list = signal([]); - private readonly _loading_domains = signal(false); - private readonly _domain_error = signal(null); private readonly _selected_domains = new Map< string, WritableSignal >(); private readonly _filters = new Map>(); - private readonly _sorts = new Map>(); + private readonly _sorts = new Map>(); private _domain_request: Promise | null = null; public readonly domain_list = this._domain_list.asReadonly(); - public readonly loading_domains = this._loading_domains.asReadonly(); - public readonly domain_error = this._domain_error.asReadonly(); public selectedDomain(key: string): WritableSignal { let selected = this._selected_domains.get(key); @@ -38,42 +33,27 @@ export class AdminDataService { return filter; } - public sort(key: string): WritableSignal { + /** Table sort of an admin page, kept while the app is open */ + public sort(key: string): WritableSignal { let sort = this._sorts.get(key); if (!sort) { - sort = signal(['', false]); + sort = signal(null); this._sorts.set(key, sort); } return sort; } - public sortBy(key: string, new_field: string) { - const sort = this.sort(key); - const [field, asc] = sort(); - if (field === new_field) { - sort.set(asc ? ['', false] : [new_field, true]); - } else { - sort.set([new_field, false]); - } - } - public async loadDomains(limit = 500): Promise { if (this._domain_list().length) return this._domain_list(); if (this._domain_request) return this._domain_request; - this._loading_domains.set(true); - this._domain_error.set(null); this._domain_request = queryDomains({ limit }) .then((response) => response.data || []) - .catch((error) => { - this._domain_error.set(error); - return [] as PlaceDomain[]; - }) + .catch(() => [] as PlaceDomain[]) .then((domains) => { this._domain_list.set(domains); return domains; }) .finally(() => { - this._loading_domains.set(false); this._domain_request = null; }); return this._domain_request; diff --git a/src/app/admin/admin-domain-select.component.ts b/src/app/admin/admin-domain-select.component.ts new file mode 100644 index 000000000..2d879a011 --- /dev/null +++ b/src/app/admin/admin-domain-select.component.ts @@ -0,0 +1,63 @@ +import { + booleanAttribute, + Component, + computed, + inject, + input, + output, +} from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatSelectModule } from '@angular/material/select'; +import { PlaceDomain } from '@placeos/ts-client'; +import { TranslatePipe } from '../ui/translate.pipe'; +import { AdminDataService } from './admin-data.service'; + +/** Domain picker for admin pages. AdminDataService keeps the choice per `key`. */ +@Component({ + selector: 'admin-domain-select', + template: ` + + + @if (all()) { + + {{ 'ADMIN.ALL_DOMAINS' | translate }} + + } + @for (domain of domain_list(); track domain.id) { + {{ domain.name }} + } + + + `, + imports: [FormsModule, MatFormFieldModule, MatSelectModule, TranslatePipe], +}) +export class AdminDomainSelectComponent { + private _data = inject(AdminDataService); + + /** Admin page that owns the selection */ + public readonly key = input.required(); + /** Add an "All domains" option */ + public readonly all = input(false, { transform: booleanAttribute }); + public readonly field_class = input('no-subscript w-56'); + /** Emits after the user picks a domain */ + public readonly changed = output(); + + public readonly domain_list = this._data.domain_list; + public readonly domain = computed(() => + this._data.selectedDomain(this.key())(), + ); + + public select(domain: PlaceDomain) { + this._data.setDomain(this.key(), domain); + this.changed.emit(domain); + } +} diff --git a/src/app/admin/admin.component.ts b/src/app/admin/admin.component.ts index 6e70ce58a..04cad210b 100644 --- a/src/app/admin/admin.component.ts +++ b/src/app/admin/admin.component.ts @@ -3,7 +3,6 @@ import { Component, OnInit, computed, inject } from '@angular/core'; import { MatRippleModule } from '@angular/material/core'; import { RouterModule } from '@angular/router'; import { extensionsForItem } from '../common/api'; -import { AsyncHandler } from '../common/async-handler.class'; import { PlaceDebugService } from '../common/debug.service'; import { ActiveItemService } from '../common/item.service'; import { i18n } from '../common/locale.service'; @@ -80,12 +79,6 @@ import { TranslatePipe } from '../ui/translate.pipe'; width: 100%; height: 100%; } - - .active { - background-color: var(--secondary) !important; - color: var(--secondary-content); - margin-right: -1px; - } `, ], imports: [ @@ -97,7 +90,7 @@ import { TranslatePipe } from '../ui/translate.pipe'; RouterModule, ], }) -export class PlaceComponent extends AsyncHandler implements OnInit { +export class PlaceComponent implements OnInit { private _settings = inject(SettingsService); private _service = inject(ActiveItemService); private _debug = inject(PlaceDebugService); diff --git a/src/app/admin/admin.routes.ts b/src/app/admin/admin.routes.ts index a551839da..ec5865be8 100644 --- a/src/app/admin/admin.routes.ts +++ b/src/app/admin/admin.routes.ts @@ -1,13 +1,13 @@ import { Routes } from '@angular/router'; -import { AuthorisedAdminGuard } from '../ui/guards/authorised-admin.guard'; +import { authorisedAdminGuard } from '../ui/guards/authorised.guard'; export const ROUTES: Routes = [ { path: '', loadComponent: () => import('./admin.component').then((m) => m.PlaceComponent), - canActivate: [AuthorisedAdminGuard], + canActivate: [authorisedAdminGuard], children: [ { path: 'about', diff --git a/src/app/admin/api-keys/api-key-modal.component.ts b/src/app/admin/api-keys/api-key-modal.component.ts index c6a3be488..18cff9055 100644 --- a/src/app/admin/api-keys/api-key-modal.component.ts +++ b/src/app/admin/api-keys/api-key-modal.component.ts @@ -31,6 +31,7 @@ import { showUser, } from '@placeos/ts-client'; import { AsyncHandler } from '../../common/async-handler.class'; +import { getInvalidSignalFields } from '../../common/forms'; import { i18n } from '../../common/locale.service'; import { notifyError } from '../../common/notifications'; import { DialogEvent } from '../../common/types'; @@ -540,7 +541,7 @@ export class APIKeyModalComponent extends AsyncHandler implements OnInit { if (this.form().invalid()) { return notifyError( i18n('COMMON.INVALID_FIELDS', { - field_list: this._invalidFields().join(', '), + field_list: getInvalidSignalFields(this.form).join(', '), }), ); } @@ -590,12 +591,4 @@ export class APIKeyModalComponent extends AsyncHandler implements OnInit { private _userLabel(user: PlaceUser) { return user?.name || user?.email || ''; } - - private _invalidFields() { - const fields: string[] = []; - if (this.form.name().invalid()) fields.push('name'); - if (this.form.user_id().invalid()) fields.push('user_id'); - if (this.form.scopes().invalid()) fields.push('scopes'); - return fields; - } } diff --git a/src/app/admin/api-keys/api-keys.component.ts b/src/app/admin/api-keys/api-keys.component.ts index 0b30326da..c7cbacfb4 100644 --- a/src/app/admin/api-keys/api-keys.component.ts +++ b/src/app/admin/api-keys/api-keys.component.ts @@ -1,11 +1,8 @@ import { Clipboard } from '@angular/cdk/clipboard'; import { Component, OnInit, inject } from '@angular/core'; -import { FormsModule } from '@angular/forms'; import { MatRippleModule } from '@angular/material/core'; -import { MatFormFieldModule } from '@angular/material/form-field'; import { MatProgressBarModule } from '@angular/material/progress-bar'; -import { MatSelectModule } from '@angular/material/select'; import { MatTooltipModule } from '@angular/material/tooltip'; import { i18n } from '../../common/locale.service'; import { notifyInfo } from '../../common/notifications'; @@ -16,6 +13,7 @@ import { SimpleTableComponent } from '../../ui/simple-table.component'; import { TranslatePipe } from '../../ui/translate.pipe'; import { APIKeyService } from './api-keys.service'; +import { AdminDomainSelectComponent } from '../admin-domain-select.component'; @Component({ selector: 'admin-api-keys', template: ` @@ -25,20 +23,7 @@ import { APIKeyService } from './api-keys.service'; {{ 'ADMIN.APP_KEYS_HEADER' | translate }}
- - - @for (domain of domain_list(); track domain.id) { - - {{ domain.name }} - - } - - +
- - - @for (domain of domain_list(); track domain.id) { - - {{ domain.name }} - - } - - +
- - - @for (domain of domain_list(); track domain.id) { - - {{ domain.name }} - - } - - +
- - - {{ - 'ADMIN.ALL_DOMAINS' | translate - }} - @for (domain of domain_list(); track domain.id) { - - {{ domain.name }} - - } - - +
- - - @for (domain of domain_list(); track domain.id) { - - {{ domain.name }} - - } - - +
-
+
- @if (uploads_list()?.length > 0) { -
-
-
- -
-
- -
-
- -
-
- -
-
-
- -
-
-
- {{ upload.file_name }} -
-
-
- {{ upload.mime_type }} -
-
- {{ sizeOf(upload.file_size) }} -
-
- {{ +upload.created_at * 1000 | dateFrom }} -
-
- - - - -
-
-
-
+ @if (uploads_list().length > 0) { + } @else {
+ +
+
+ {{ upload.file_name }} +
+
+
+ +
{{ upload.mime_type }}
+
+ +
+ {{ sizeOf(upload.file_size) }} +
+
+ +
{{ +upload.created_at * 1000 | dateFrom }}
+
+ +
+ + + + +
+
`, - styles: [ - ` - [header] icon { - opacity: 0; - } - [header] button.active icon { - opacity: 1; - } - [header] button:hover icon { - opacity: 0.3; - } - `, - ], imports: [ + AdminDomainSelectComponent, LoadingLabelDirective, IconComponent, - ScrollingModule, + SimpleTableComponent, TranslatePipe, MatRippleModule, MatTooltipModule, MatProgressBarModule, MatFormFieldModule, MatInputModule, - MatSelectModule, FormsModule, DateFromPipe, ], @@ -433,23 +293,8 @@ export class UploadLibraryComponent extends AsyncHandler implements OnInit { public readonly domain = this._admin_data.selectedDomain('upload-library'); public readonly refresh = signal(0); public readonly loading = signal(false); - public readonly sorted_by = this._admin_data.sort('upload-library:uploads'); - public readonly sorted_uploads = computed(() => { - const [field, asc] = this.sorted_by(); - if (!field) return this.uploads_list(); - return [...this.uploads_list()].sort((a, b) => { - if (!field) return 0; - const value_a = a[field]; - const value_b = b[field]; - const is_number = typeof value_a === 'number'; - let result = 0; - if (is_number) result = value_a - value_b; - else result = `${value_a}`.localeCompare(`${value_b}`); - return result * (asc ? 1 : -1); - }); - }); - - public readonly domain_list = this._admin_data.domain_list; + /** Table sort, kept when the user leaves the page */ + public readonly sort = this._admin_data.sort('upload-library:uploads'); private readonly _uploads_list = resource({ params: () => ({ @@ -500,17 +345,6 @@ export class UploadLibraryComponent extends AsyncHandler implements OnInit { return `${short_bytes} ${sizes[level]}`; } - /** Sort state of a column for screen readers */ - public ariaSort(field: string) { - const [sorted_field, asc] = this.sorted_by(); - if (sorted_field !== field) return null; - return asc ? 'ascending' : 'descending'; - } - - public sortBy(new_field: string) { - this._admin_data.sortBy('upload-library:uploads', new_field); - } - public copyLink(upload: UploadInfo) { this._clipboard.copy( `${location.protocol}//${location.host}/api/engine/v2/uploads/${upload.id}/url`, @@ -616,7 +450,8 @@ export class UploadLibraryComponent extends AsyncHandler implements OnInit { } public async removeUpload(upload: UploadInfo) { - const result = await openConfirmModal( + const removed = await confirmAndRun( + this._dialog, { title: i18n('ADMIN.UPLOADS_LIB_REMOVE'), content: i18n('ADMIN.UPLOADS_LIB_REMOVE_MSG', { @@ -624,23 +459,17 @@ export class UploadLibraryComponent extends AsyncHandler implements OnInit { }), icon: { type: 'icon', content: 'delete' }, }, - this._dialog, + { + loading: i18n('ADMIN.UPLOADS_LIB_REMOVE_LOADING'), + action: () => + remove({ + id: upload.id, + query_params: {}, + path: 'uploads', + }), + error: (reason) => `Failed to remove upload. Error: ${reason}`, + }, ); - if (result?.reason !== 'done') return; - result.loading(i18n('ADMIN.UPLOADS_LIB_REMOVE_LOADING')); - try { - await remove({ - id: upload.id, - query_params: {}, - path: 'uploads', - }); - } catch (err) { - return notifyError( - `Failed to remove upload. Error: ${await readError(err)}`, - ); - } finally { - result.close(); - } - this.refresh.update((value) => value + 1); + if (removed) this.refresh.update((value) => value + 1); } } diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index ebd122c23..0f980e541 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,70 +1,72 @@ import { Route } from '@angular/router'; -import { AuthorisedAdminGuard } from './ui/guards/authorised-admin.guard'; -import { AuthorisedUserGuard } from './ui/guards/authorised-user.guard'; +import { + authorisedAdminGuard, + authorisedUserGuard, +} from './ui/guards/authorised.guard'; import { UnauthorisedComponent } from './ui/unauthorised.component'; export const appRoutes: Route[] = [ { path: 'unauthorised', component: UnauthorisedComponent }, { path: 'modules', - canActivate: [AuthorisedUserGuard], + canActivate: [authorisedUserGuard], loadChildren: () => import('./modules/modules.routes').then((m) => m.ROUTES), }, { path: 'domains', - canActivate: [AuthorisedAdminGuard], + canActivate: [authorisedAdminGuard], loadChildren: () => import('./domains/domains.routes').then((m) => m.ROUTES), }, { path: 'drivers', data: { role_only: true }, - canActivate: [AuthorisedUserGuard], + canActivate: [authorisedUserGuard], loadChildren: () => import('./drivers/drivers.routes').then((m) => m.ROUTES), }, { path: 'groups', - canActivate: [AuthorisedAdminGuard], + canActivate: [authorisedAdminGuard], loadChildren: () => import('./groups/groups.routes').then((m) => m.ROUTES), }, { path: 'systems', - canActivate: [AuthorisedUserGuard], + canActivate: [authorisedUserGuard], loadChildren: () => import('./systems/systems.routes').then((m) => m.ROUTES), }, { path: 'repositories', - canActivate: [AuthorisedAdminGuard], + canActivate: [authorisedAdminGuard], loadChildren: () => import('./repositories/repositories.routes').then((m) => m.ROUTES), }, { path: 'triggers', data: { role_only: true }, - canActivate: [AuthorisedUserGuard], + canActivate: [authorisedUserGuard], loadChildren: () => import('./triggers/triggers.routes').then((m) => m.ROUTES), }, { path: 'users', data: { allow_subsystem: true }, - canActivate: [AuthorisedAdminGuard], + canActivate: [authorisedAdminGuard], loadChildren: () => import('./users/users.routes').then((m) => m.ROUTES), }, { path: 'zones', - canActivate: [AuthorisedUserGuard], + canActivate: [authorisedUserGuard], loadChildren: () => import('./zones/zones.routes').then((m) => m.ROUTES), }, { path: 'admin', - canActivate: [AuthorisedAdminGuard], + canActivate: [authorisedAdminGuard], loadChildren: () => import('./admin/admin.routes').then((m) => m.ROUTES), }, diff --git a/src/app/app.ts b/src/app/app.ts index 65bf3a3a1..cbf224915 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -17,8 +17,7 @@ import { RouterOutlet, } from '@angular/router'; import { setupCache, updateAvailable } from './common/application'; -import { AsyncHandler } from './common/async-handler.class'; -import { detectIE, log } from './common/general'; +import { log } from './common/general'; import { setNotifyOutlet } from './common/notifications'; import { PlaceSettings, setLoadingMessage, setupPlace } from './common/placeos'; import { initSentry } from './common/sentry'; @@ -157,7 +156,7 @@ function browserOnline() { MatTooltip, ], }) -export class AppComponent extends AsyncHandler implements OnInit { +export class AppComponent implements OnInit { private _settings = inject(SettingsService); private _users = inject(BackofficeUsersService); private _cache = inject(SwUpdate); @@ -185,11 +184,6 @@ export class AppComponent extends AsyncHandler implements OnInit { public async ngOnInit() { setLoadingMessage('Initialising application...'); - /* istanbul ignore if */ - if (detectIE() && detectIE() < 12) { - location.href = `${location.origin}${location.pathname}assets/not-supported.html`; - return; - } this._route.queryParamMap.subscribe((params) => { if (params.has('lang')) { const locale = params.get('lang'); diff --git a/src/app/common/forms.ts b/src/app/common/forms.ts index 2fbe4303f..3992fb01d 100644 --- a/src/app/common/forms.ts +++ b/src/app/common/forms.ts @@ -1,5 +1,17 @@ -import { FieldTree } from '@angular/forms/signals'; +import { + afterNextRender, + DestroyRef, + inject, + WritableSignal, +} from '@angular/core'; +import { FieldTree, submit } from '@angular/forms/signals'; import { MatChipEditedEvent, MatChipInputEvent } from '@angular/material/chips'; +import { cleanObject } from '@placeos/ts-client'; +import { readError } from './errors'; +import { HotkeysService } from './hotkeys.service'; +import { i18n } from './locale.service'; +import { notifyError, notifySuccess } from './notifications'; +import { DialogEvent, Identity } from './types'; export function addSignalChipItem( items: T[], @@ -52,3 +64,79 @@ export function getInvalidSignalFields( }) .map(fieldLabel); } + +/** + * Form values merged over the item to save. Updates also drop the values in + * `empty`, so unset fields keep their saved value. + */ +export function mergeFormItem( + item: { id?: string; toJSON?: () => object }, + form_value: T, + empty: unknown[] = [undefined], +): Identity & T { + const merged = { ...(item.toJSON ? item.toJSON() : item), ...form_value }; + return (item.id ? cleanObject(merged, empty) : merged) as Identity & T; +} + +/** Parts of a form dialog that `submitFormDialog` drives */ +export interface FormDialog { + /** Saving label. Null when idle. */ + loading: WritableSignal; + event: { emit: (event: DialogEvent) => void }; + close: () => void; +} + +/** + * Validate a dialog form and save it. Shows the saving label while it saves. + * On success emits `done` with the saved item, notifies and closes the dialog. + * @param prefix Locale key prefix of `SAVING`, `SAVE_SUCCESS` and + * `SAVE_ERROR`, e.g. `ZONES.` or `ADMIN.BROKERS_` + * @param save Builds the payload and saves it. Return `undefined` to stop + * without saving. + * @param after Runs after a save, before the dialog closes + */ +export async function submitFormDialog( + form: FieldTree, + dialog: FormDialog, + prefix: string, + save: () => Promise, + after?: (item: R) => Promise, +) { + await submit(form, async () => { + dialog.loading.set(i18n(`${prefix}SAVING`)); + try { + const item = await save(); + if (item === undefined) return dialog.loading.set(null); + dialog.event.emit({ reason: 'done', metadata: { item } }); + notifySuccess(i18n(`${prefix}SAVE_SUCCESS`)); + await after?.(item); + dialog.close(); + } catch (err) { + dialog.loading.set(null); + notifyError( + i18n(`${prefix}SAVE_ERROR`, { error: await readError(err) }), + ); + } + }); + if (form().invalid()) { + notifyError( + i18n('COMMON.INVALID_FIELDS', { + field_list: getInvalidSignalFields(form).join(', '), + }), + ); + } +} + +/** + * Run `fn` when the user presses the save hotkey, until the calling component + * is destroyed. Call from a constructor or field. It starts listening after + * the first render, so a dialog is open by then and owns the hotkey. + */ +export function onSaveHotkey(fn: () => void) { + const hotkeys = inject(HotkeysService); + const destroy_ref = inject(DestroyRef); + afterNextRender(() => { + const sub = hotkeys.listen(['KeyS'], fn); + destroy_ref.onDestroy(() => sub?.unsubscribe()); + }); +} diff --git a/src/app/common/general.ts b/src/app/common/general.ts index 861415f99..2cc2784d2 100644 --- a/src/app/common/general.ts +++ b/src/app/common/general.ts @@ -37,37 +37,6 @@ export function log( } } -/* istanbul ignore next */ -/** - * detect IE - * returns version of IE or false, if browser is not Internet Explorer - */ -export function detectIE(): number { - const ua = window.navigator.userAgent; - - const msie = ua.indexOf('MSIE '); - if (msie > 0) { - // IE 10 or older => return version number - return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10); - } - - const trident = ua.indexOf('Trident/'); - if (trident > 0) { - // IE 11 => return version number - const rv = ua.indexOf('rv:'); - return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10); - } - - const edge = ua.indexOf('Edge/'); - if (edge > 0) { - // Edge (IE 12+) => return version number - return parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10); - } - - // other browser - return 0; -} - /** * Get item from the nested object * @param keys List of sub-keys to search for @@ -179,7 +148,6 @@ export function numberToPosition(num: number): string { return `${num}th`; } -export const csvToJson = parseCSV; /** * Parses a CSV string back into an array of JavaScript objects. * - It assumes the first row is the header row. @@ -358,28 +326,6 @@ export function downloadFile(filename: string, contents: string) { document.body.removeChild(element); } -/* istanbul ignore next */ -/** - * Flatten nested array - * @param an_array Array to flatten - */ -export function flatten(an_array: T[]) { - const stack = [...an_array]; - const res = []; - while (stack.length) { - // pop value from stack - const next = stack.pop(); - if (Array.isArray(next)) { - // push back array items, won't modify the original input - stack.push(...next); - } else { - res.push(next); - } - } - // reverse to restore input order - return res.reverse(); -} - export const issueDescription = (hash, date) => ` **Describe the bug** A clear and concise description of what the bug is. @@ -413,19 +359,3 @@ If applicable, add screenshots to help explain your problem. **Hash:** ${hash} **Built:** ${date} `; - -/** - * Pad the start of a string or number with given character - * @param value String or number to pad - * @param length Length of the final string - * @param character Character to pad output string with. Defaults to `0`. - */ -export function padLength( - value: number | string, - length = 2, - character = '0', -): string { - let str = `${value}`; - while (str.length < length) str = `${character}${str}`; - return str; -} diff --git a/src/app/common/item.service.ts b/src/app/common/item.service.ts index ac96e7f81..59347f602 100644 --- a/src/app/common/item.service.ts +++ b/src/app/common/item.service.ts @@ -60,6 +60,16 @@ const receiptItems = (resources: CascadeResource[]) => name: resource.name, })); +/** Types a copy would lose data for. Trigger actions and conditions are getters that a copy drops. */ +const NO_COPY_TYPES = ['triggers']; +/** Types with unique fields that a duplicate cannot change, e.g. user email */ +const NO_DUPLICATE_TYPES = [ + 'users', + 'domains', + 'repositories', + ...NO_COPY_TYPES, +]; + /** Resource types whose items carry settings */ const SETTINGS_TYPES = new Set([ 'drivers', @@ -156,6 +166,12 @@ export class ActiveItemService extends AsyncHandler { return this._type; } + /** Whether the active item can be copied, or duplicated without edits */ + public canCopy(duplicate = false) { + const blocked = duplicate ? NO_DUPLICATE_TYPES : NO_COPY_TYPES; + return this.canMutate(2) && !blocked.includes(this._type); + } + public canMutate(permission: number) { if (this._user.current()?.sys_admin) return true; return ( @@ -269,7 +285,7 @@ export class ActiveItemService extends AsyncHandler { } public create(item?: PlaceResource, copy = false) { - if (!this.canMutate(2)) return; + if (!(copy ? this.canCopy() : this.canMutate(2))) return; item = item || this._active_item(); const actions = Object.values(ACTIONS).find( @@ -576,7 +592,7 @@ export class ActiveItemService extends AsyncHandler { } public duplicate() { - if (!this._user.current().sys_admin) return; + if (!this.canCopy(true)) return; const item = this._active_item(); if (item) { const ref = this._dialog.open(DuplicateModalComponent, { @@ -588,8 +604,10 @@ export class ActiveItemService extends AsyncHandler { ref.componentInstance.event.subscribe((e: DialogEvent) => { if (e.reason === 'done' && e.metadata?.[0]) { // Clear first so the copy is shown with its own settings + const copy = e.metadata[0] as unknown as Identity; this._active_item.set(null); - this.replaceItem(e.metadata[0] as unknown as Identity); + this.replaceItem(copy); + this._router.navigate([`/${this._type}`, copy.id, 'about']); } }); } diff --git a/src/app/common/signals.ts b/src/app/common/signals.ts index c3bc93c18..8a149ac45 100644 --- a/src/app/common/signals.ts +++ b/src/app/common/signals.ts @@ -1,5 +1,6 @@ -import { DestroyRef, Injector, Signal, inject, signal } from '@angular/core'; +import { DestroyRef, Signal, inject, signal } from '@angular/core'; import type { Signal as ClientSignal } from '@placeos/ts-client'; +import type { DialogEvent } from './types'; export interface SubscriptionLike { unsubscribe(): void; @@ -17,18 +18,6 @@ function disposeSubscription(subscription: SubscriptionLike | (() => void)) { } } -export function signalFromSubscribable( - source: Subscribable, - initialValue: T, - injector = inject(Injector), -): Signal { - const destroyRef = injector.get(DestroyRef); - const state = signal(initialValue); - const subscription = source.subscribe((value) => state.set(value)); - destroyRef.onDestroy(() => disposeSubscription(subscription)); - return state.asReadonly(); -} - export function signalFromClient( source: ClientSignal, initialValue: T = source.value, @@ -42,18 +31,6 @@ export function signalFromClient( return state.asReadonly(); } -export function toSignal( - source: Signal | Subscribable, - options: { initialValue?: T; injector?: Injector } = {}, -): Signal { - if (typeof source === 'function') return source as Signal; - return signalFromSubscribable( - source, - options.initialValue, - options.injector, - ); -} - export function waitForEvent( source: Subscribable, predicate: (value: T) => boolean = () => true, @@ -111,3 +88,20 @@ export function waitForClientSignalValue( predicate, ); } + +/** + * Wait until a dialog emits an event with the given `reason`, or closes. + * Resolves to that event, or to the close result when the dialog closes first. + */ +export function waitForDialog( + ref: { + componentInstance: { event: Subscribable }; + afterClosed: () => Subscribable; + }, + reason: DialogEvent['reason'], +): Promise | undefined> { + return Promise.race([ + waitForEvent(ref.componentInstance.event, (e) => e.reason === reason), + waitForEvent(ref.afterClosed()), + ]) as Promise | undefined>; +} diff --git a/src/app/common/timezone-helpers.ts b/src/app/common/timezone-helpers.ts deleted file mode 100644 index da240b2f8..000000000 --- a/src/app/common/timezone-helpers.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { padLength } from './general'; - -const TIMZONE_OFFSET_STRINGS = {}; - -export function getTimezoneOffsetString(tz: string) { - if (TIMZONE_OFFSET_STRINGS[tz]) return TIMZONE_OFFSET_STRINGS[tz]; - const offset = getTimezoneOffsetInMinutes(tz); - const hours = Math.floor(Math.abs(offset) / 60); - const minutes = Math.abs(offset) % 60; - const output = `${offset >= 0 ? '+' : '-'}${padLength(hours, 2)}${padLength( - minutes, - 2, - )}`; - TIMZONE_OFFSET_STRINGS[tz] = output; - return output; -} - -/** - * Offset of the given timezone from UTC in minutes, at the given date. - * Positive values are ahead of UTC, e.g. `Asia/Kolkata` is `330`. - */ -export function getTimezoneOffsetInMinutes(timeZone, date = new Date()) { - // `longOffset` always gives a numeric offset (e.g. "GMT+05:30"), where - // `short` gives an abbreviation (e.g. "AEST") for many zones. - // A fixed locale keeps the digits and format predictable. - const formatter = new Intl.DateTimeFormat('en-US', { - timeZone, - timeZoneName: 'longOffset', - }); - const tz_offset_part = formatter - .formatToParts(date) - .find((part) => part.type === 'timeZoneName'); - // UTC is given as plain "GMT", which does not match - const offset_match = tz_offset_part?.value.match( - /GMT([+-])(\d{2}):(\d{2})/, - ); - if (!offset_match) return 0; - - const sign = offset_match[1] === '+' ? 1 : -1; - const hours = parseInt(offset_match[2], 10); - const minutes = parseInt(offset_match[3], 10); - - return sign * (hours * 60 + minutes); -} diff --git a/src/app/common/timezones.ts b/src/app/common/timezones.ts index 59cb3e378..adc7393bc 100644 --- a/src/app/common/timezones.ts +++ b/src/app/common/timezones.ts @@ -1,1259 +1,3 @@ -import { flatten, unique } from './general'; - -export const TIMEZONES = [ - { - value: 'Dateline Standard Time', - abbr: 'DST', - offset: -12, - isdst: false, - text: '(UTC-12:00) International Date Line West', - utc: ['Etc/GMT+12'], - }, - { - value: 'UTC-11', - abbr: 'U', - offset: -11, - isdst: false, - text: '(UTC-11:00) Coordinated Universal Time-11', - utc: [ - 'Etc/GMT+11', - 'Pacific/Midway', - 'Pacific/Niue', - 'Pacific/Pago_Pago', - ], - }, - { - value: 'Hawaiian Standard Time', - abbr: 'HST', - offset: -10, - isdst: false, - text: '(UTC-10:00) Hawaii', - utc: [ - 'Etc/GMT+10', - 'Pacific/Honolulu', - 'Pacific/Johnston', - 'Pacific/Rarotonga', - 'Pacific/Tahiti', - ], - }, - { - value: 'Alaskan Standard Time', - abbr: 'AKDT', - offset: -8, - isdst: true, - text: '(UTC-09:00) Alaska', - utc: [ - 'America/Anchorage', - 'America/Juneau', - 'America/Nome', - 'America/Sitka', - 'America/Yakutat', - ], - }, - { - value: 'Pacific Standard Time (Mexico)', - abbr: 'PDT', - offset: -7, - isdst: true, - text: '(UTC-08:00) Baja California', - utc: ['America/Santa_Isabel'], - }, - { - value: 'Pacific Daylight Time', - abbr: 'PDT', - offset: -7, - isdst: true, - text: '(UTC-07:00) Pacific Time (US & Canada)', - utc: [ - 'America/Dawson', - 'America/Los_Angeles', - 'America/Tijuana', - 'America/Vancouver', - 'America/Whitehorse', - ], - }, - { - value: 'Pacific Standard Time', - abbr: 'PST', - offset: -8, - isdst: false, - text: '(UTC-08:00) Pacific Time (US & Canada)', - utc: [ - 'America/Dawson', - 'America/Los_Angeles', - 'America/Tijuana', - 'America/Vancouver', - 'America/Whitehorse', - 'PST8PDT', - ], - }, - { - value: 'US Mountain Standard Time', - abbr: 'UMST', - offset: -7, - isdst: false, - text: '(UTC-07:00) Arizona', - utc: [ - 'America/Creston', - 'America/Dawson_Creek', - 'America/Hermosillo', - 'America/Phoenix', - 'Etc/GMT+7', - ], - }, - { - value: 'Mountain Standard Time (Mexico)', - abbr: 'MDT', - offset: -6, - isdst: true, - text: '(UTC-07:00) Chihuahua, La Paz, Mazatlan', - utc: ['America/Chihuahua', 'America/Mazatlan'], - }, - { - value: 'Mountain Standard Time', - abbr: 'MDT', - offset: -6, - isdst: true, - text: '(UTC-07:00) Mountain Time (US & Canada)', - utc: [ - 'America/Boise', - 'America/Cambridge_Bay', - 'America/Denver', - 'America/Edmonton', - 'America/Inuvik', - 'America/Ojinaga', - 'America/Yellowknife', - 'MST7MDT', - ], - }, - { - value: 'Central America Standard Time', - abbr: 'CAST', - offset: -6, - isdst: false, - text: '(UTC-06:00) Central America', - utc: [ - 'America/Belize', - 'America/Costa_Rica', - 'America/El_Salvador', - 'America/Guatemala', - 'America/Managua', - 'America/Tegucigalpa', - 'Etc/GMT+6', - 'Pacific/Galapagos', - ], - }, - { - value: 'Central Standard Time', - abbr: 'CDT', - offset: -5, - isdst: true, - text: '(UTC-06:00) Central Time (US & Canada)', - utc: [ - 'America/Chicago', - 'America/Indiana/Knox', - 'America/Indiana/Tell_City', - 'America/Matamoros', - 'America/Menominee', - 'America/North_Dakota/Beulah', - 'America/North_Dakota/Center', - 'America/North_Dakota/New_Salem', - 'America/Rainy_River', - 'America/Rankin_Inlet', - 'America/Resolute', - 'America/Winnipeg', - 'CST6CDT', - ], - }, - { - value: 'Central Standard Time (Mexico)', - abbr: 'CDT', - offset: -5, - isdst: true, - text: '(UTC-06:00) Guadalajara, Mexico City, Monterrey', - utc: [ - 'America/Bahia_Banderas', - 'America/Cancun', - 'America/Merida', - 'America/Mexico_City', - 'America/Monterrey', - ], - }, - { - value: 'Canada Central Standard Time', - abbr: 'CCST', - offset: -6, - isdst: false, - text: '(UTC-06:00) Saskatchewan', - utc: ['America/Regina', 'America/Swift_Current'], - }, - { - value: 'SA Pacific Standard Time', - abbr: 'SPST', - offset: -5, - isdst: false, - text: '(UTC-05:00) Bogota, Lima, Quito', - utc: [ - 'America/Bogota', - 'America/Cayman', - 'America/Coral_Harbour', - 'America/Eirunepe', - 'America/Guayaquil', - 'America/Jamaica', - 'America/Lima', - 'America/Panama', - 'America/Rio_Branco', - 'Etc/GMT+5', - ], - }, - { - value: 'Eastern Standard Time', - abbr: 'EDT', - offset: -4, - isdst: true, - text: '(UTC-05:00) Eastern Time (US & Canada)', - utc: [ - 'America/Detroit', - 'America/Havana', - 'America/Indiana/Petersburg', - 'America/Indiana/Vincennes', - 'America/Indiana/Winamac', - 'America/Iqaluit', - 'America/Kentucky/Monticello', - 'America/Louisville', - 'America/Montreal', - 'America/Nassau', - 'America/New_York', - 'America/Nipigon', - 'America/Pangnirtung', - 'America/Port-au-Prince', - 'America/Thunder_Bay', - 'America/Toronto', - 'EST5EDT', - ], - }, - { - value: 'US Eastern Standard Time', - abbr: 'UEDT', - offset: -4, - isdst: true, - text: '(UTC-05:00) Indiana (East)', - utc: [ - 'America/Indiana/Marengo', - 'America/Indiana/Vevay', - 'America/Indianapolis', - ], - }, - { - value: 'Venezuela Standard Time', - abbr: 'VST', - offset: -4.5, - isdst: false, - text: '(UTC-04:30) Caracas', - utc: ['America/Caracas'], - }, - { - value: 'Paraguay Standard Time', - abbr: 'PYT', - offset: -4, - isdst: false, - text: '(UTC-04:00) Asuncion', - utc: ['America/Asuncion'], - }, - { - value: 'Atlantic Standard Time', - abbr: 'ADT', - offset: -3, - isdst: true, - text: '(UTC-04:00) Atlantic Time (Canada)', - utc: [ - 'America/Glace_Bay', - 'America/Goose_Bay', - 'America/Halifax', - 'America/Moncton', - 'America/Thule', - 'Atlantic/Bermuda', - ], - }, - { - value: 'Central Brazilian Standard Time', - abbr: 'CBST', - offset: -4, - isdst: false, - text: '(UTC-04:00) Cuiaba', - utc: ['America/Campo_Grande', 'America/Cuiaba'], - }, - { - value: 'SA Western Standard Time', - abbr: 'SWST', - offset: -4, - isdst: false, - text: '(UTC-04:00) Georgetown, La Paz, Manaus, San Juan', - utc: [ - 'America/Anguilla', - 'America/Antigua', - 'America/Aruba', - 'America/Barbados', - 'America/Blanc-Sablon', - 'America/Boa_Vista', - 'America/Curacao', - 'America/Dominica', - 'America/Grand_Turk', - 'America/Grenada', - 'America/Guadeloupe', - 'America/Guyana', - 'America/Kralendijk', - 'America/La_Paz', - 'America/Lower_Princes', - 'America/Manaus', - 'America/Marigot', - 'America/Martinique', - 'America/Montserrat', - 'America/Port_of_Spain', - 'America/Porto_Velho', - 'America/Puerto_Rico', - 'America/Santo_Domingo', - 'America/St_Barthelemy', - 'America/St_Kitts', - 'America/St_Lucia', - 'America/St_Thomas', - 'America/St_Vincent', - 'America/Tortola', - 'Etc/GMT+4', - ], - }, - { - value: 'Pacific SA Standard Time', - abbr: 'PSST', - offset: -4, - isdst: false, - text: '(UTC-04:00) Santiago', - utc: ['America/Santiago', 'Antarctica/Palmer'], - }, - { - value: 'Newfoundland Standard Time', - abbr: 'NDT', - offset: -2.5, - isdst: true, - text: '(UTC-03:30) Newfoundland', - utc: ['America/St_Johns'], - }, - { - value: 'E. South America Standard Time', - abbr: 'ESAST', - offset: -3, - isdst: false, - text: '(UTC-03:00) Brasilia', - utc: ['America/Sao_Paulo'], - }, - { - value: 'Argentina Standard Time', - abbr: 'AST', - offset: -3, - isdst: false, - text: '(UTC-03:00) Buenos Aires', - utc: [ - 'America/Argentina/La_Rioja', - 'America/Argentina/Rio_Gallegos', - 'America/Argentina/Salta', - 'America/Argentina/San_Juan', - 'America/Argentina/San_Luis', - 'America/Argentina/Tucuman', - 'America/Argentina/Ushuaia', - 'America/Buenos_Aires', - 'America/Catamarca', - 'America/Cordoba', - 'America/Jujuy', - 'America/Mendoza', - ], - }, - { - value: 'SA Eastern Standard Time', - abbr: 'SEST', - offset: -3, - isdst: false, - text: '(UTC-03:00) Cayenne, Fortaleza', - utc: [ - 'America/Araguaina', - 'America/Belem', - 'America/Cayenne', - 'America/Fortaleza', - 'America/Maceio', - 'America/Paramaribo', - 'America/Recife', - 'America/Santarem', - 'Antarctica/Rothera', - 'Atlantic/Stanley', - 'Etc/GMT+3', - ], - }, - { - value: 'Greenland Standard Time', - abbr: 'GDT', - offset: -3, - isdst: true, - text: '(UTC-03:00) Greenland', - utc: ['America/Godthab'], - }, - { - value: 'Montevideo Standard Time', - abbr: 'MST', - offset: -3, - isdst: false, - text: '(UTC-03:00) Montevideo', - utc: ['America/Montevideo'], - }, - { - value: 'Bahia Standard Time', - abbr: 'BST', - offset: -3, - isdst: false, - text: '(UTC-03:00) Salvador', - utc: ['America/Bahia'], - }, - { - value: 'UTC-02', - abbr: 'U', - offset: -2, - isdst: false, - text: '(UTC-02:00) Coordinated Universal Time-02', - utc: ['America/Noronha', 'Atlantic/South_Georgia', 'Etc/GMT+2'], - }, - { - value: 'Mid-Atlantic Standard Time', - abbr: 'MDT', - offset: -1, - isdst: true, - text: '(UTC-02:00) Mid-Atlantic - Old', - utc: [], - }, - { - value: 'Azores Standard Time', - abbr: 'ADT', - offset: 0, - isdst: true, - text: '(UTC-01:00) Azores', - utc: ['America/Scoresbysund', 'Atlantic/Azores'], - }, - { - value: 'Cape Verde Standard Time', - abbr: 'CVST', - offset: -1, - isdst: false, - text: '(UTC-01:00) Cape Verde Is.', - utc: ['Atlantic/Cape_Verde', 'Etc/GMT+1'], - }, - { - value: 'Morocco Standard Time', - abbr: 'MDT', - offset: 1, - isdst: true, - text: '(UTC) Casablanca', - utc: ['Africa/Casablanca', 'Africa/El_Aaiun'], - }, - { - value: 'UTC', - abbr: 'UTC', - offset: 0, - isdst: false, - text: '(UTC) Coordinated Universal Time', - utc: ['America/Danmarkshavn', 'Etc/GMT'], - }, - { - value: 'GMT Standard Time', - abbr: 'GMT', - offset: 0, - isdst: false, - text: '(UTC) Edinburgh, London', - utc: [ - 'Europe/Isle_of_Man', - 'Europe/Guernsey', - 'Europe/Jersey', - 'Europe/London', - ], - }, - { - value: 'British Summer Time', - abbr: 'BST', - offset: 1, - isdst: true, - text: '(UTC+01:00) Edinburgh, London', - utc: [ - 'Europe/Isle_of_Man', - 'Europe/Guernsey', - 'Europe/Jersey', - 'Europe/London', - ], - }, - { - value: 'GMT Standard Time', - abbr: 'GDT', - offset: 1, - isdst: true, - text: '(UTC) Dublin, Lisbon', - utc: [ - 'Atlantic/Canary', - 'Atlantic/Faeroe', - 'Atlantic/Madeira', - 'Europe/Dublin', - 'Europe/Lisbon', - ], - }, - { - value: 'Greenwich Standard Time', - abbr: 'GST', - offset: 0, - isdst: false, - text: '(UTC) Monrovia, Reykjavik', - utc: [ - 'Africa/Abidjan', - 'Africa/Accra', - 'Africa/Bamako', - 'Africa/Banjul', - 'Africa/Bissau', - 'Africa/Conakry', - 'Africa/Dakar', - 'Africa/Freetown', - 'Africa/Lome', - 'Africa/Monrovia', - 'Africa/Nouakchott', - 'Africa/Ouagadougou', - 'Africa/Sao_Tome', - 'Atlantic/Reykjavik', - 'Atlantic/St_Helena', - ], - }, - { - value: 'W. Europe Standard Time', - abbr: 'WEDT', - offset: 2, - isdst: true, - text: '(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna', - utc: [ - 'Arctic/Longyearbyen', - 'Europe/Amsterdam', - 'Europe/Andorra', - 'Europe/Berlin', - 'Europe/Busingen', - 'Europe/Gibraltar', - 'Europe/Luxembourg', - 'Europe/Malta', - 'Europe/Monaco', - 'Europe/Oslo', - 'Europe/Rome', - 'Europe/San_Marino', - 'Europe/Stockholm', - 'Europe/Vaduz', - 'Europe/Vatican', - 'Europe/Vienna', - 'Europe/Zurich', - ], - }, - { - value: 'Central Europe Standard Time', - abbr: 'CEDT', - offset: 2, - isdst: true, - text: '(UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague', - utc: [ - 'Europe/Belgrade', - 'Europe/Bratislava', - 'Europe/Budapest', - 'Europe/Ljubljana', - 'Europe/Podgorica', - 'Europe/Prague', - 'Europe/Tirane', - ], - }, - { - value: 'Romance Standard Time', - abbr: 'RDT', - offset: 2, - isdst: true, - text: '(UTC+01:00) Brussels, Copenhagen, Madrid, Paris', - utc: [ - 'Africa/Ceuta', - 'Europe/Brussels', - 'Europe/Copenhagen', - 'Europe/Madrid', - 'Europe/Paris', - ], - }, - { - value: 'Central European Standard Time', - abbr: 'CEDT', - offset: 2, - isdst: true, - text: '(UTC+01:00) Sarajevo, Skopje, Warsaw, Zagreb', - utc: [ - 'Europe/Sarajevo', - 'Europe/Skopje', - 'Europe/Warsaw', - 'Europe/Zagreb', - ], - }, - { - value: 'W. Central Africa Standard Time', - abbr: 'WCAST', - offset: 1, - isdst: false, - text: '(UTC+01:00) West Central Africa', - utc: [ - 'Africa/Algiers', - 'Africa/Bangui', - 'Africa/Brazzaville', - 'Africa/Douala', - 'Africa/Kinshasa', - 'Africa/Lagos', - 'Africa/Libreville', - 'Africa/Luanda', - 'Africa/Malabo', - 'Africa/Ndjamena', - 'Africa/Niamey', - 'Africa/Porto-Novo', - 'Africa/Tunis', - 'Etc/GMT-1', - ], - }, - { - value: 'Namibia Standard Time', - abbr: 'NST', - offset: 1, - isdst: false, - text: '(UTC+01:00) Windhoek', - utc: ['Africa/Windhoek'], - }, - { - value: 'GTB Standard Time', - abbr: 'GDT', - offset: 3, - isdst: true, - text: '(UTC+02:00) Athens, Bucharest', - utc: [ - 'Asia/Nicosia', - 'Europe/Athens', - 'Europe/Bucharest', - 'Europe/Chisinau', - ], - }, - { - value: 'Middle East Standard Time', - abbr: 'MEDT', - offset: 3, - isdst: true, - text: '(UTC+02:00) Beirut', - utc: ['Asia/Beirut'], - }, - { - value: 'Egypt Standard Time', - abbr: 'EST', - offset: 2, - isdst: false, - text: '(UTC+02:00) Cairo', - utc: ['Africa/Cairo'], - }, - { - value: 'Syria Standard Time', - abbr: 'SDT', - offset: 3, - isdst: true, - text: '(UTC+02:00) Damascus', - utc: ['Asia/Damascus'], - }, - { - value: 'E. Europe Standard Time', - abbr: 'EEDT', - offset: 3, - isdst: true, - text: '(UTC+02:00) E. Europe', - utc: [ - 'Asia/Nicosia', - 'Europe/Athens', - 'Europe/Bucharest', - 'Europe/Chisinau', - 'Europe/Helsinki', - 'Europe/Kiev', - 'Europe/Mariehamn', - 'Europe/Nicosia', - 'Europe/Riga', - 'Europe/Sofia', - 'Europe/Tallinn', - 'Europe/Uzhgorod', - 'Europe/Vilnius', - 'Europe/Zaporozhye', - ], - }, - { - value: 'South Africa Standard Time', - abbr: 'SAST', - offset: 2, - isdst: false, - text: '(UTC+02:00) Harare, Pretoria', - utc: [ - 'Africa/Blantyre', - 'Africa/Bujumbura', - 'Africa/Gaborone', - 'Africa/Harare', - 'Africa/Johannesburg', - 'Africa/Kigali', - 'Africa/Lubumbashi', - 'Africa/Lusaka', - 'Africa/Maputo', - 'Africa/Maseru', - 'Africa/Mbabane', - 'Etc/GMT-2', - ], - }, - { - value: 'FLE Standard Time', - abbr: 'FDT', - offset: 3, - isdst: true, - text: '(UTC+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius', - utc: [ - 'Europe/Helsinki', - 'Europe/Kiev', - 'Europe/Mariehamn', - 'Europe/Riga', - 'Europe/Sofia', - 'Europe/Tallinn', - 'Europe/Uzhgorod', - 'Europe/Vilnius', - 'Europe/Zaporozhye', - ], - }, - { - value: 'Turkey Standard Time', - abbr: 'TDT', - offset: 3, - isdst: false, - text: '(UTC+03:00) Istanbul', - utc: ['Europe/Istanbul'], - }, - { - value: 'Israel Standard Time', - abbr: 'JDT', - offset: 3, - isdst: true, - text: '(UTC+02:00) Jerusalem', - utc: ['Asia/Jerusalem'], - }, - { - value: 'Libya Standard Time', - abbr: 'LST', - offset: 2, - isdst: false, - text: '(UTC+02:00) Tripoli', - utc: ['Africa/Tripoli'], - }, - { - value: 'Jordan Standard Time', - abbr: 'JST', - offset: 3, - isdst: false, - text: '(UTC+03:00) Amman', - utc: ['Asia/Amman'], - }, - { - value: 'Arabic Standard Time', - abbr: 'AST', - offset: 3, - isdst: false, - text: '(UTC+03:00) Baghdad', - utc: ['Asia/Baghdad'], - }, - { - value: 'Kaliningrad Standard Time', - abbr: 'KST', - offset: 3, - isdst: false, - text: '(UTC+02:00) Kaliningrad', - utc: ['Europe/Kaliningrad'], - }, - { - value: 'Arab Standard Time', - abbr: 'AST', - offset: 3, - isdst: false, - text: '(UTC+03:00) Kuwait, Riyadh', - utc: [ - 'Asia/Aden', - 'Asia/Bahrain', - 'Asia/Kuwait', - 'Asia/Qatar', - 'Asia/Riyadh', - ], - }, - { - value: 'E. Africa Standard Time', - abbr: 'EAST', - offset: 3, - isdst: false, - text: '(UTC+03:00) Nairobi', - utc: [ - 'Africa/Addis_Ababa', - 'Africa/Asmera', - 'Africa/Dar_es_Salaam', - 'Africa/Djibouti', - 'Africa/Juba', - 'Africa/Kampala', - 'Africa/Khartoum', - 'Africa/Mogadishu', - 'Africa/Nairobi', - 'Antarctica/Syowa', - 'Etc/GMT-3', - 'Indian/Antananarivo', - 'Indian/Comoro', - 'Indian/Mayotte', - ], - }, - { - value: 'Moscow Standard Time', - abbr: 'MSK', - offset: 3, - isdst: false, - text: '(UTC+03:00) Moscow, St. Petersburg, Volgograd, Minsk', - utc: [ - 'Europe/Kirov', - 'Europe/Moscow', - 'Europe/Simferopol', - 'Europe/Volgograd', - 'Europe/Minsk', - ], - }, - { - value: 'Samara Time', - abbr: 'SAMT', - offset: 4, - isdst: false, - text: '(UTC+04:00) Samara, Ulyanovsk, Saratov', - utc: ['Europe/Astrakhan', 'Europe/Samara', 'Europe/Ulyanovsk'], - }, - { - value: 'Iran Standard Time', - abbr: 'IDT', - offset: 4.5, - isdst: true, - text: '(UTC+03:30) Tehran', - utc: ['Asia/Tehran'], - }, - { - value: 'Arabian Standard Time', - abbr: 'AST', - offset: 4, - isdst: false, - text: '(UTC+04:00) Abu Dhabi, Muscat', - utc: ['Asia/Dubai', 'Asia/Muscat', 'Etc/GMT-4'], - }, - { - value: 'Azerbaijan Standard Time', - abbr: 'ADT', - offset: 5, - isdst: true, - text: '(UTC+04:00) Baku', - utc: ['Asia/Baku'], - }, - { - value: 'Mauritius Standard Time', - abbr: 'MST', - offset: 4, - isdst: false, - text: '(UTC+04:00) Port Louis', - utc: ['Indian/Mahe', 'Indian/Mauritius', 'Indian/Reunion'], - }, - { - value: 'Georgian Standard Time', - abbr: 'GET', - offset: 4, - isdst: false, - text: '(UTC+04:00) Tbilisi', - utc: ['Asia/Tbilisi'], - }, - { - value: 'Caucasus Standard Time', - abbr: 'CST', - offset: 4, - isdst: false, - text: '(UTC+04:00) Yerevan', - utc: ['Asia/Yerevan'], - }, - { - value: 'Afghanistan Standard Time', - abbr: 'AST', - offset: 4.5, - isdst: false, - text: '(UTC+04:30) Kabul', - utc: ['Asia/Kabul'], - }, - { - value: 'West Asia Standard Time', - abbr: 'WAST', - offset: 5, - isdst: false, - text: '(UTC+05:00) Ashgabat, Tashkent', - utc: [ - 'Antarctica/Mawson', - 'Asia/Aqtau', - 'Asia/Aqtobe', - 'Asia/Ashgabat', - 'Asia/Dushanbe', - 'Asia/Oral', - 'Asia/Samarkand', - 'Asia/Tashkent', - 'Etc/GMT-5', - 'Indian/Kerguelen', - 'Indian/Maldives', - ], - }, - { - value: 'Yekaterinburg Time', - abbr: 'YEKT', - offset: 5, - isdst: false, - text: '(UTC+05:00) Yekaterinburg', - utc: ['Asia/Yekaterinburg'], - }, - { - value: 'Pakistan Standard Time', - abbr: 'PKT', - offset: 5, - isdst: false, - text: '(UTC+05:00) Islamabad, Karachi', - utc: ['Asia/Karachi'], - }, - { - value: 'India Standard Time', - abbr: 'IST', - offset: 5.5, - isdst: false, - text: '(UTC+05:30) Chennai, Kolkata, Mumbai, New Delhi', - utc: ['Asia/Kolkata'], - }, - { - value: 'Sri Lanka Standard Time', - abbr: 'SLST', - offset: 5.5, - isdst: false, - text: '(UTC+05:30) Sri Jayawardenepura', - utc: ['Asia/Colombo'], - }, - { - value: 'Nepal Standard Time', - abbr: 'NST', - offset: 5.75, - isdst: false, - text: '(UTC+05:45) Kathmandu', - utc: ['Asia/Kathmandu'], - }, - { - value: 'Central Asia Standard Time', - abbr: 'CAST', - offset: 6, - isdst: false, - text: '(UTC+06:00) Nur-Sultan (Astana)', - utc: [ - 'Antarctica/Vostok', - 'Asia/Almaty', - 'Asia/Bishkek', - 'Asia/Qyzylorda', - 'Asia/Urumqi', - 'Etc/GMT-6', - 'Indian/Chagos', - ], - }, - { - value: 'Bangladesh Standard Time', - abbr: 'BST', - offset: 6, - isdst: false, - text: '(UTC+06:00) Dhaka', - utc: ['Asia/Dhaka', 'Asia/Thimphu'], - }, - { - value: 'Myanmar Standard Time', - abbr: 'MST', - offset: 6.5, - isdst: false, - text: '(UTC+06:30) Yangon (Rangoon)', - utc: ['Asia/Rangoon', 'Indian/Cocos'], - }, - { - value: 'SE Asia Standard Time', - abbr: 'SAST', - offset: 7, - isdst: false, - text: '(UTC+07:00) Bangkok, Hanoi, Jakarta', - utc: [ - 'Antarctica/Davis', - 'Asia/Bangkok', - 'Asia/Hovd', - 'Asia/Jakarta', - 'Asia/Phnom_Penh', - 'Asia/Pontianak', - 'Asia/Saigon', - 'Asia/Vientiane', - 'Etc/GMT-7', - 'Indian/Christmas', - ], - }, - { - value: 'N. Central Asia Standard Time', - abbr: 'NCAST', - offset: 7, - isdst: false, - text: '(UTC+07:00) Novosibirsk', - utc: ['Asia/Novokuznetsk', 'Asia/Novosibirsk', 'Asia/Omsk'], - }, - { - value: 'China Standard Time', - abbr: 'CST', - offset: 8, - isdst: false, - text: '(UTC+08:00) Beijing, Chongqing, Hong Kong, Urumqi', - utc: ['Asia/Hong_Kong', 'Asia/Macau', 'Asia/Shanghai'], - }, - { - value: 'North Asia Standard Time', - abbr: 'NAST', - offset: 8, - isdst: false, - text: '(UTC+08:00) Krasnoyarsk', - utc: ['Asia/Krasnoyarsk'], - }, - { - value: 'Singapore Standard Time', - abbr: 'MPST', - offset: 8, - isdst: false, - text: '(UTC+08:00) Kuala Lumpur, Singapore', - utc: [ - 'Asia/Brunei', - 'Asia/Kuala_Lumpur', - 'Asia/Kuching', - 'Asia/Makassar', - 'Asia/Manila', - 'Asia/Singapore', - 'Etc/GMT-8', - ], - }, - { - value: 'W. Australia Standard Time', - abbr: 'WAST', - offset: 8, - isdst: false, - text: '(UTC+08:00) Perth', - utc: ['Antarctica/Casey', 'Australia/Perth'], - }, - { - value: 'Taipei Standard Time', - abbr: 'TST', - offset: 8, - isdst: false, - text: '(UTC+08:00) Taipei', - utc: ['Asia/Taipei'], - }, - { - value: 'Ulaanbaatar Standard Time', - abbr: 'UST', - offset: 8, - isdst: false, - text: '(UTC+08:00) Ulaanbaatar', - utc: ['Asia/Choibalsan', 'Asia/Ulaanbaatar'], - }, - { - value: 'North Asia East Standard Time', - abbr: 'NAEST', - offset: 8, - isdst: false, - text: '(UTC+08:00) Irkutsk', - utc: ['Asia/Irkutsk'], - }, - { - value: 'Japan Standard Time', - abbr: 'JST', - offset: 9, - isdst: false, - text: '(UTC+09:00) Osaka, Sapporo, Tokyo', - utc: [ - 'Asia/Dili', - 'Asia/Jayapura', - 'Asia/Tokyo', - 'Etc/GMT-9', - 'Pacific/Palau', - ], - }, - { - value: 'Korea Standard Time', - abbr: 'KST', - offset: 9, - isdst: false, - text: '(UTC+09:00) Seoul', - utc: ['Asia/Pyongyang', 'Asia/Seoul'], - }, - { - value: 'Cen. Australia Standard Time', - abbr: 'CAST', - offset: 9.5, - isdst: false, - text: '(UTC+09:30) Adelaide', - utc: ['Australia/Adelaide', 'Australia/Broken_Hill'], - }, - { - value: 'AUS Central Standard Time', - abbr: 'ACST', - offset: 9.5, - isdst: false, - text: '(UTC+09:30) Darwin', - utc: ['Australia/Darwin'], - }, - { - value: 'E. Australia Standard Time', - abbr: 'EAST', - offset: 10, - isdst: false, - text: '(UTC+10:00) Brisbane', - utc: ['Australia/Brisbane', 'Australia/Lindeman'], - }, - { - value: 'AUS Eastern Standard Time', - abbr: 'AEST', - offset: 10, - isdst: false, - text: '(UTC+10:00) Canberra, Melbourne, Sydney', - utc: ['Australia/Melbourne', 'Australia/Sydney'], - }, - { - value: 'West Pacific Standard Time', - abbr: 'WPST', - offset: 10, - isdst: false, - text: '(UTC+10:00) Guam, Port Moresby', - utc: [ - 'Antarctica/DumontDUrville', - 'Etc/GMT-10', - 'Pacific/Guam', - 'Pacific/Port_Moresby', - 'Pacific/Saipan', - 'Pacific/Truk', - ], - }, - { - value: 'Tasmania Standard Time', - abbr: 'TST', - offset: 10, - isdst: false, - text: '(UTC+10:00) Hobart', - utc: ['Australia/Currie', 'Australia/Hobart'], - }, - { - value: 'Yakutsk Standard Time', - abbr: 'YST', - offset: 9, - isdst: false, - text: '(UTC+09:00) Yakutsk', - utc: ['Asia/Chita', 'Asia/Khandyga', 'Asia/Yakutsk'], - }, - { - value: 'Central Pacific Standard Time', - abbr: 'CPST', - offset: 11, - isdst: false, - text: '(UTC+11:00) Solomon Is., New Caledonia', - utc: [ - 'Antarctica/Macquarie', - 'Etc/GMT-11', - 'Pacific/Efate', - 'Pacific/Guadalcanal', - 'Pacific/Kosrae', - 'Pacific/Noumea', - 'Pacific/Ponape', - ], - }, - { - value: 'Vladivostok Standard Time', - abbr: 'VST', - offset: 11, - isdst: false, - text: '(UTC+11:00) Vladivostok', - utc: ['Asia/Sakhalin', 'Asia/Ust-Nera', 'Asia/Vladivostok'], - }, - { - value: 'New Zealand Standard Time', - abbr: 'NZST', - offset: 12, - isdst: false, - text: '(UTC+12:00) Auckland, Wellington', - utc: ['Antarctica/McMurdo', 'Pacific/Auckland'], - }, - { - value: 'UTC+12', - abbr: 'U', - offset: 12, - isdst: false, - text: '(UTC+12:00) Coordinated Universal Time+12', - utc: [ - 'Etc/GMT-12', - 'Pacific/Funafuti', - 'Pacific/Kwajalein', - 'Pacific/Majuro', - 'Pacific/Nauru', - 'Pacific/Tarawa', - 'Pacific/Wake', - 'Pacific/Wallis', - ], - }, - { - value: 'Fiji Standard Time', - abbr: 'FST', - offset: 12, - isdst: false, - text: '(UTC+12:00) Fiji', - utc: ['Pacific/Fiji'], - }, - { - value: 'Magadan Standard Time', - abbr: 'MST', - offset: 12, - isdst: false, - text: '(UTC+12:00) Magadan', - utc: [ - 'Asia/Anadyr', - 'Asia/Kamchatka', - 'Asia/Magadan', - 'Asia/Srednekolymsk', - ], - }, - { - value: 'Kamchatka Standard Time', - abbr: 'KDT', - offset: 13, - isdst: true, - text: '(UTC+12:00) Petropavlovsk-Kamchatsky - Old', - utc: ['Asia/Kamchatka'], - }, - { - value: 'Tonga Standard Time', - abbr: 'TST', - offset: 13, - isdst: false, - text: "(UTC+13:00) Nuku'alofa", - utc: [ - 'Etc/GMT-13', - 'Pacific/Enderbury', - 'Pacific/Fakaofo', - 'Pacific/Tongatapu', - ], - }, - { - value: 'Samoa Standard Time', - abbr: 'SST', - offset: 13, - isdst: false, - text: '(UTC+13:00) Samoa', - utc: ['Pacific/Apia'], - }, -]; - -export const TIMEZONES_IANA = unique( - flatten(TIMEZONES.map((_) => _.utc)), -).sort(); +/** Sorted IANA timezone names that the browser supports. Used for timezone autocomplete. */ +export const TIMEZONES_IANA: readonly string[] = + Intl.supportedValuesOf('timeZone'); diff --git a/src/app/domains/application-form.component.ts b/src/app/domains/application-form.component.ts index ab1f1529b..6c6c10253 100644 --- a/src/app/domains/application-form.component.ts +++ b/src/app/domains/application-form.component.ts @@ -11,31 +11,28 @@ import { signal, } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { form, FormField, submit } from '@angular/forms/signals'; +import { form, FormField } from '@angular/forms/signals'; import { MatChipInputEvent, MatChipsModule } from '@angular/material/chips'; import { addApplication, - cleanObject, PlaceApplication, PlaceResource, updateApplication, } from '@placeos/ts-client'; -import { AsyncHandler } from '../common/async-handler.class'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { Md5 } from 'ts-md5'; -import { readError } from '../common/errors'; import { addSignalChipItem, - getInvalidSignalFields, + mergeFormItem, + onSaveHotkey, removeSignalChipItem, + submitFormDialog, } from '../common/forms'; -import { HotkeysService } from '../common/hotkeys.service'; import { i18n } from '../common/locale.service'; -import { notifyError, notifySuccess } from '../common/notifications'; -import { DialogEvent, Identity } from '../common/types'; +import { DialogEvent } from '../common/types'; import { FullscreenModalShellComponent } from '../ui/fullscreen-modal-shell.component'; import { IconComponent } from '../ui/icon.component'; import { SettingsToggleComponent } from '../ui/settings-toggle.component'; @@ -227,14 +224,13 @@ import { FullscreenModalShellComponent, ], }) -export class ApplicationFormComponent extends AsyncHandler implements OnInit { +export class ApplicationFormComponent implements OnInit { private _dialog_ref = inject>(MatDialogRef); private _data = inject<{ item: PlaceApplication; readonly?: string }>( MAT_DIALOG_DATA, ); private readonly _name = 'DOMAINS.APPLICATION'; - private _hotkey = inject(HotkeysService); private _injector = inject(Injector); @Output() public event = new EventEmitter(); @@ -260,6 +256,10 @@ export class ApplicationFormComponent extends AsyncHandler implements OnInit { subsystems: removeSignalChipItem(value.subsystems, i), })); + constructor() { + onSaveHotkey(() => this.submit()); + } + public ngOnInit(): void { const item = this._data.item; const edit = !!item.id; @@ -289,51 +289,30 @@ export class ApplicationFormComponent extends AsyncHandler implements OnInit { }, { injector: this._injector }, ); - this.subscription( - 'save_item_key', - this._hotkey.listen(['KeyS'], () => this.submit()), - ); } public async submit(): Promise { - await submit(this.form, async () => { - const item = this._data.item as unknown as PlaceResource; - this.loading.set(i18n(`${this._name}_SAVING`)); - const item_json = item.toJSON ? item.toJSON() : item; - const form_item = ( - item.id - ? cleanObject({ ...item_json, ...this.formModel() }, [ - undefined, - ]) - : { ...item_json, ...this.formModel() } - ) as Identity; - const save_item = { ...form_item, uid: this.client_id() }; - delete (save_item as Identity & { client_id?: unknown }).client_id; - try { - const _item = await (save_item.id - ? updateApplication( - save_item.id as string, - save_item as unknown as PlaceApplication, - ) - : addApplication(save_item as unknown as PlaceApplication)); - this.event.emit({ reason: 'done', metadata: { item: _item } }); - notifySuccess(i18n(`${this._name}_SAVE_SUCCESS`)); - this._dialog_ref.close(); - } catch (err) { - this.loading.set(null); - notifyError( - i18n(`${this._name}_SAVE_ERROR`, { - error: await readError(err), - }), - ); - } - }); - if (this.form().invalid()) { - return notifyError( - i18n('COMMON.INVALID_FIELDS', { - field_list: getInvalidSignalFields(this.form).join(', '), - }), - ); - } + await submitFormDialog( + this.form, + { + loading: this.loading, + event: this.event, + close: () => this._dialog_ref.close(), + }, + `${this._name}_`, + () => { + const save_item = { + ...mergeFormItem( + this._data.item as unknown as PlaceResource, + this.formModel(), + ), + uid: this.client_id(), + } as unknown as PlaceApplication & { client_id?: unknown }; + delete save_item.client_id; + return save_item.id + ? updateApplication(save_item.id, save_item) + : addApplication(save_item); + }, + ); } } diff --git a/src/app/domains/auth-sources.utilities.ts b/src/app/domains/auth-sources.utilities.ts index bcda242d7..ce9b29598 100644 --- a/src/app/domains/auth-sources.utilities.ts +++ b/src/app/domains/auth-sources.utilities.ts @@ -52,11 +52,6 @@ export interface LDAPSourceFormModel { filter: string; } -export type AuthSourceFormModel = - | OAuthSourceFormModel - | SAMLSourceFormModel - | LDAPSourceFormModel; - /** * Generate form model for an OAuth authentication source * @param auth_source Auth source to apply changes to diff --git a/src/app/domains/domain-about.component.ts b/src/app/domains/domain-about.component.ts index e790e6001..ce0fba426 100644 --- a/src/app/domains/domain-about.component.ts +++ b/src/app/domains/domain-about.component.ts @@ -8,7 +8,6 @@ import { Clipboard } from '@angular/cdk/clipboard'; import { MatRippleModule } from '@angular/material/core'; import { MatTabsModule } from '@angular/material/tabs'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { AsyncHandler } from '../common/async-handler.class'; import { readError } from '../common/errors'; import { i18n } from '../common/locale.service'; import { notifyError, notifySuccess } from '../common/notifications'; @@ -22,84 +21,88 @@ import { DomainStateService } from './domain-state.service'; @Component({ selector: 'app-domain-about', template: ` - @if (item?.description) { -
-

- {{ 'COMMON.FIELD_DESCRIPTION' | translate }} -

-
-
-
- } - @if (item?.email_domains?.length) { -
+
+ @if (item?.description) { +
+

+ {{ 'COMMON.FIELD_DESCRIPTION' | translate }} +

+
+
+
+ } + @if (item?.email_domains?.length) {
- {{ 'DOMAINS.EMAIL_DOMAINS' | translate }} -
- @for (domain of item?.email_domains; track domain) { - - } -
- } -
-

- {{ 'COMMON.SETTINGS' | translate }} -

- + } +
+ } +
- save - -
- @if (form) { -
- + {{ 'COMMON.SETTINGS' | translate }} + +
- } + save + + + @if (form) { +
+ + + + + @if (index !== 1) { + + } + @if (index === 1) { + + } +
+ } +
`, styles: [ ` @@ -121,7 +124,7 @@ import { DomainStateService } from './domain-state.service'; MarkdownPipe, ], }) -export class DomainAboutComponent extends AsyncHandler { +export class DomainAboutComponent { private _service = inject(DomainStateService); private _clipboard = inject(Clipboard); @@ -153,7 +156,6 @@ export class DomainAboutComponent extends AsyncHandler { } constructor() { - super(); effect(() => { if (this._item()) this.loadForm(); }); diff --git a/src/app/domains/domain-applications.component.ts b/src/app/domains/domain-applications.component.ts index f293d0353..0b5a8c29e 100644 --- a/src/app/domains/domain-applications.component.ts +++ b/src/app/domains/domain-applications.component.ts @@ -19,7 +19,7 @@ import { DomainStateService } from './domain-state.service'; selector: 'domain-applications', template: `
-
+
-
+
-
+
-
+
= {}; public readonly newAuthSource = () => this._service.editAuthSource(); public readonly editAuthSource = (source) => this._service.editAuthSource(source); public readonly removeAuthSource = (source) => this._service.deleteAuthSource(source); - - public get item(): PlaceDomain { - return this._service.active_item as PlaceDomain; - } } diff --git a/src/app/domains/domain-form.component.ts b/src/app/domains/domain-form.component.ts index 4bb56f570..652f134bd 100644 --- a/src/app/domains/domain-form.component.ts +++ b/src/app/domains/domain-form.component.ts @@ -4,36 +4,25 @@ import { computed, EventEmitter, inject, - OnInit, Output, signal, } from '@angular/core'; -import { form, FormField, submit } from '@angular/forms/signals'; +import { form, FormField } from '@angular/forms/signals'; import { MatChipInputEvent, MatChipsModule } from '@angular/material/chips'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; -import { - addDomain, - cleanObject, - PlaceDomain, - updateDomain, -} from '@placeos/ts-client'; -import { AsyncHandler } from '../common/async-handler.class'; -import { readError } from '../common/errors'; +import { addDomain, PlaceDomain, updateDomain } from '@placeos/ts-client'; import { addSignalChipItem, - getInvalidSignalFields, + mergeFormItem, + onSaveHotkey, removeSignalChipItem, + submitFormDialog, } from '../common/forms'; -import { HotkeysService } from '../common/hotkeys.service'; import { i18n } from '../common/locale.service'; -import { - notifyError, - notifySuccess, - notifyWarn, -} from '../common/notifications'; -import { DialogEvent, Identity } from '../common/types'; +import { notifyWarn } from '../common/notifications'; +import { DialogEvent } from '../common/types'; import { isValidDomain } from '../common/validation'; import { FullscreenModalShellComponent } from '../ui/fullscreen-modal-shell.component'; import { IconComponent } from '../ui/icon.component'; @@ -278,13 +267,12 @@ import { FullscreenModalShellComponent, ], }) -export class DomainFormComponent extends AsyncHandler implements OnInit { +export class DomainFormComponent { private _dialog_ref = inject>(MatDialogRef); private _data = inject<{ item: PlaceDomain; readonly?: string }>( MAT_DIALOG_DATA, ); - private _hotkey = inject(HotkeysService); private readonly _name = 'DOMAINS'; @Output() public event = new EventEmitter(); @@ -320,50 +308,28 @@ export class DomainFormComponent extends AsyncHandler implements OnInit { () => this.formModel().email_domains || [], ); - public ngOnInit(): void { - this.subscription( - 'save_item_key', - this._hotkey.listen(['KeyS'], () => this.submit()), - ); + constructor() { + onSaveHotkey(() => this.submit()); } public async submit(): Promise { - await submit(this.form, async () => { - const item = this._data.item; - this.loading.set(i18n(`${this._name}.SAVING`)); - const item_json = item.toJSON ? item.toJSON() : item; - const form_item = ( - item.id - ? cleanObject({ ...item_json, ...this.formModel() }, [ - undefined, - ]) - : { ...item_json, ...this.formModel() } - ) as Identity; - try { - const _item = await (form_item.id - ? updateDomain( - form_item.id as string, - form_item as unknown as PlaceDomain, - ) - : addDomain(form_item as unknown as PlaceDomain)); - this.event.emit({ reason: 'done', metadata: { item: _item } }); - notifySuccess(i18n(`${this._name}.SAVE_SUCCESS`)); - this._dialog_ref.close(); - } catch (err) { - this.loading.set(null); - notifyError( - i18n(`${this._name}.SAVE_ERROR`, { - error: await readError(err), - }), - ); - } - }); - if (this.form().invalid()) { - return notifyError( - i18n('COMMON.INVALID_FIELDS', { - field_list: getInvalidSignalFields(this.form).join(', '), - }), - ); - } + await submitFormDialog( + this.form, + { + loading: this.loading, + event: this.event, + close: () => this._dialog_ref.close(), + }, + `${this._name}.`, + () => { + const form_item = mergeFormItem( + this._data.item, + this.formModel(), + ) as unknown as PlaceDomain; + return form_item.id + ? updateDomain(form_item.id, form_item) + : addDomain(form_item); + }, + ); } } diff --git a/src/app/domains/domain-state.service.ts b/src/app/domains/domain-state.service.ts index 9776dc1d6..8b9d28669 100644 --- a/src/app/domains/domain-state.service.ts +++ b/src/app/domains/domain-state.service.ts @@ -37,8 +37,8 @@ import { ActiveItemService } from '../common/item.service'; import { reportLoadError } from '../common/load-error'; import { i18n } from '../common/locale.service'; import { notifyError, notifySuccess } from '../common/notifications'; -import { waitForEvent } from '../common/signals'; -import { DialogEvent, FormModalComponent, Identity } from '../common/types'; +import { waitForDialog } from '../common/signals'; +import { FormModalComponent, Identity } from '../common/types'; import { AuthSourceModalComponent } from '../overlays/auth-source-modal.component'; import { openConfirmModal } from '../overlays/confirm-modal.component'; import { ApplicationFormComponent } from './application-form.component'; @@ -252,13 +252,7 @@ export class DomainStateService { }, }); const instance = ref.componentInstance as unknown as FormModalComponent; - const details = await Promise.race([ - waitForEvent( - instance.event, - (_: DialogEvent) => _.reason === 'done', - ), - waitForEvent(ref.afterClosed()), - ]); + const details = await waitForDialog(ref, 'done'); if (details?.reason !== 'done') return; this._changed.set(new Date().valueOf()); } @@ -300,13 +294,7 @@ export class DomainStateService { domain: this.active_item, }, }); - const details = await Promise.race([ - waitForEvent( - ref.componentInstance.event, - (_: DialogEvent) => _.reason === 'done', - ), - waitForEvent(ref.afterClosed()), - ]); + const details = await waitForDialog(ref, 'done'); if (details?.reason !== 'done') return; this._changed.set(new Date().valueOf()); } diff --git a/src/app/domains/domain-users.component.ts b/src/app/domains/domain-users.component.ts index 6a921492b..384ff2589 100644 --- a/src/app/domains/domain-users.component.ts +++ b/src/app/domains/domain-users.component.ts @@ -1,5 +1,4 @@ import { Component, inject } from '@angular/core'; -import { PlaceDomain } from '@placeos/ts-client'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { LoadingLabelDirective } from '../ui/loading-label.directive'; @@ -10,7 +9,7 @@ import { DomainStateService } from './domain-state.service'; @Component({ selector: 'domain-users', template: ` -
+
- -
+ `, - styles: [``], - imports: [ - MatTabNavPanel, - IconComponent, - MatTooltipModule, - MatRipple, - TranslatePipe, - RouterModule, - ItemTablistComponent, - ItemDetailsComponent, - ItemDetailsSkeletonComponent, - ItemSelectionComponent, - ItemSidebarComponent, - SidebarMenuComponent, - ], + imports: [ItemShellComponent], }) -export class DomainsComponent extends AsyncHandler { +export class DomainsComponent { private _service = inject(DomainStateService); - protected _item = inject(ActiveItemService); public readonly name = 'domains'; - public open_menu = false; - public readonly scroll = signal(0); - public readonly loading = this._item.loading; public readonly counts = this._service.counts; public readonly item = this._service.item; public readonly extra_actions = [ @@ -152,6 +59,4 @@ export class DomainsComponent extends AsyncHandler { ...extensionsForItem(this.item(), this.name), ]; }); - - public readonly newItem = () => this._item.create(); } diff --git a/src/app/drivers/driver-about.component.ts b/src/app/drivers/driver-about.component.ts index 940b284f7..24a0a0b55 100644 --- a/src/app/drivers/driver-about.component.ts +++ b/src/app/drivers/driver-about.component.ts @@ -1,5 +1,5 @@ import { Clipboard } from '@angular/cdk/clipboard'; -import { AsyncPipe, DatePipe } from '@angular/common'; +import { AsyncPipe } from '@angular/common'; import { Component, inject } from '@angular/core'; import { MatRippleModule } from '@angular/material/core'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; @@ -9,8 +9,8 @@ import { i18n } from '../common/locale.service'; import { notifyInfo } from '../common/notifications'; import { SettingsFormComponent } from '../ui/forms/settings-form.component'; import { IconComponent } from '../ui/icon.component'; +import { ItemTimestampsComponent } from '../ui/item-timestamps.component'; import { LoadingLabelDirective } from '../ui/loading-label.directive'; -import { DateFromPipe } from '../ui/pipes/date-from.pipe'; import { MarkdownPipe } from '../ui/pipes/markdown.pipe'; import { TranslatePipe } from '../ui/translate.pipe'; import { DriverStateService } from './driver-state.service'; @@ -69,42 +69,10 @@ import { DriverStateService } from './driver-state.service';
{{ item()?.module_name }}
-
- {{ 'COMMON.CREATED_AT' | translate }} -
-
- - {{ item().created_at * 1000 | dateFrom }} - -
-
- {{ 'COMMON.UPDATED_AT' | translate }} -
-
- - {{ item().updated_at * 1000 | dateFrom }} - -
+
@@ -214,18 +182,17 @@ import { DriverStateService } from './driver-state.service'; `, ], imports: [ + ItemTimestampsComponent, LoadingLabelDirective, IconComponent, TranslatePipe, MatRippleModule, SettingsFormComponent, MatTooltipModule, - DateFromPipe, MarkdownPipe, RouterModule, MatProgressSpinnerModule, AsyncPipe, - DatePipe, ], }) export class DriverAboutComponent { @@ -235,7 +202,6 @@ export class DriverAboutComponent { public readonly updateDriver = () => this._service.updateDriver(); public readonly recompile = () => this._service.recompileDriver(); public readonly reload = () => this._service.reloadDriver(); - public readonly viewErrors = () => this._service.viewError(); public readonly item = this._service.item; public copyCommit(): void { diff --git a/src/app/drivers/driver-devices.component.ts b/src/app/drivers/driver-devices.component.ts index 627949c02..fc6d7bb46 100644 --- a/src/app/drivers/driver-devices.component.ts +++ b/src/app/drivers/driver-devices.component.ts @@ -9,7 +9,6 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; import { PlaceModule, PlaceSystem, querySystems } from '@placeos/ts-client'; -import { toSignal } from '../common/signals'; import { BindingDirective } from '../ui/binding.directive'; import { CustomTooltipComponent } from '../ui/custom-tooltip.component'; @@ -264,17 +263,13 @@ export class DriverModulesComponent { /** Signal holding the value of the search */ public readonly filter = model(''); /** Whether systems are being loaded */ - public readonly loading = toSignal(this._service.loading, { - initialValue: false, - }); + public readonly loading = this._service.loading; /** Currently active driver */ public readonly item = this._service.item; /** List of systems associated with modules */ public readonly systems = signal>({}); /** List of modules */ - public readonly module_list = toSignal(this._service.modules, { - initialValue: [] as PlaceModule[], - }); + public readonly module_list = this._service.modules; public readonly modules = computed(() => { const filter = this.filter().toLowerCase(); const modules = this.module_list(); diff --git a/src/app/drivers/driver-form.component.ts b/src/app/drivers/driver-form.component.ts index 5c095f4d2..566f338c8 100644 --- a/src/app/drivers/driver-form.component.ts +++ b/src/app/drivers/driver-form.component.ts @@ -11,11 +11,10 @@ import { signal, } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { form, FormField, submit } from '@angular/forms/signals'; +import { form, FormField } from '@angular/forms/signals'; import { addDriver, addSettings, - cleanObject, EncryptionLevel, GitCommitDetails, listRepositoryCommits, @@ -33,17 +32,15 @@ import { } from '@placeos/ts-client'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { AsyncHandler } from '../common/async-handler.class'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSelectModule } from '@angular/material/select'; import * as yaml from 'js-yaml'; -import { getInvalidSignalFields } from '../common/forms'; -import { HotkeysService } from '../common/hotkeys.service'; +import { mergeFormItem, onSaveHotkey, submitFormDialog } from '../common/forms'; import { i18n } from '../common/locale.service'; -import { notifyError, notifySuccess } from '../common/notifications'; +import { notifyError } from '../common/notifications'; import { DialogEvent, Identity } from '../common/types'; import { readError } from '../common/errors'; @@ -295,14 +292,13 @@ import { FullscreenModalShellComponent, ], }) -export class DriverFormComponent extends AsyncHandler implements OnInit { +export class DriverFormComponent implements OnInit { private _dialog_ref = inject>(MatDialogRef); private _data = inject<{ item: PlaceDriver; readonly?: string }>( MAT_DIALOG_DATA, ); private readonly _name = 'DRIVERS'; - private _hotkey = inject(HotkeysService); private _date_pipe = new DatePipe('en'); @Output() public event = new EventEmitter(); @@ -401,14 +397,10 @@ export class DriverFormComponent extends AsyncHandler implements OnInit { const edit = !!item.id; this.heading.set(i18n(`${this._name}.${edit ? 'EDIT' : 'NEW'}`)); this._loadDetailsFromForm(); - this.subscription( - 'save_item_key', - this._hotkey.listen(['KeyS'], () => this.submit()), - ); } constructor() { - super(); + onSaveHotkey(() => this.submit()); effect(() => { const module_name = this.formModel().module_name; const clean_name = module_name?.replace(/ /g, '_') || ''; @@ -479,55 +471,38 @@ export class DriverFormComponent extends AsyncHandler implements OnInit { } public async submit(): Promise { - await submit(this.form, async () => { - const item = this._data.item; - this.saving.set(i18n(`${this._name}.SAVING`)); - const item_json = item.toJSON ? item.toJSON() : item; - const form_value = { ...this.formModel() }; - if (item.id) { - delete form_value.role; - delete form_value.class_name; - } - const form_item = ( - item.id - ? cleanObject({ ...item_json, ...form_value }, [undefined]) - : { ...item_json, ...form_value } - ) as Identity; - try { - const _item = await (form_item.id - ? updateDriver( - form_item.id as string, - form_item as unknown as PlaceDriver, - ) - : addDriver(form_item as unknown as PlaceDriver)); - this.event.emit({ - reason: 'done', - metadata: { item: _item as unknown as Identity }, - }); - notifySuccess(i18n(`${this._name}.SAVE_SUCCESS`)); + await submitFormDialog( + this.form, + { + loading: this.saving, + event: this.event, + close: () => this._dialog_ref.close(), + }, + `${this._name}.`, + () => { + const item = this._data.item; + const form_value = { ...this.formModel() }; + if (item.id) { + delete form_value.role; + delete form_value.class_name; + } + const form_item = mergeFormItem( + item, + form_value, + ) as unknown as PlaceDriver; + return form_item.id + ? updateDriver(form_item.id, form_item) + : addDriver(form_item); + }, + async (item) => { if (!this.formModel().id && this.formModel().settings) { await this.newSettings( - _item as unknown as Identity, + item as unknown as Identity, this.formModel().settings, ); } - this._dialog_ref.close(); - } catch (err) { - this.saving.set(null); - notifyError( - i18n(`${this._name}.SAVE_ERROR`, { - error: await readError(err), - }), - ); - } - }); - if (this.form().invalid()) { - return notifyError( - i18n('COMMON.INVALID_FIELDS', { - field_list: getInvalidSignalFields(this.form).join(', '), - }), - ); - } + }, + ); } private _applyDriverDetails(details: PlaceDriverDetails) { diff --git a/src/app/drivers/driver-state.service.ts b/src/app/drivers/driver-state.service.ts index ae04af08b..e35840f5c 100644 --- a/src/app/drivers/driver-state.service.ts +++ b/src/app/drivers/driver-state.service.ts @@ -16,7 +16,7 @@ import { ActiveItemService } from '../common/item.service'; import { reportLoadError } from '../common/load-error'; import { notifyError, notifySuccess } from '../common/notifications'; import { collectPages } from '../common/support-access'; -import { HashMap, Identity } from '../common/types'; +import { Identity } from '../common/types'; import { openConfirmModal } from '../overlays/confirm-modal.component'; import { ViewResponseModalComponent } from '../overlays/view-response-modal.component'; import { DriverUpdateListModalComponent } from './driver-update-list-modal.component'; @@ -26,7 +26,6 @@ export class DriverStateService { private _state = inject(ActiveItemService); private _dialog = inject(MatDialog); - private _last_error = signal(null); private _poll = signal(0); private _modules_change = signal(0); @@ -37,8 +36,6 @@ export class DriverStateService { public readonly loading = computed( () => this._modules.isLoading() || this._docs.isLoading(), ); - /** Bumped each time a module of the driver is removed */ - public readonly modules_change = this._modules_change.asReadonly(); private readonly _updates_available = resource({ params: () => this._poll(), @@ -93,17 +90,6 @@ export class DriverStateService { this._dialog.open(DriverUpdateListModalComponent, {}); } - public viewError() { - const error = this._last_error(); - if (!error) return; - this._dialog.open( - ViewResponseModalComponent, - { - data: { title: 'Driver Compilation Error', content: error }, - }, - ); - } - public async updateDriver() { const item = this._state.active_item as PlaceDriver; if (!item.update_available) return notifyError('No update available.'); @@ -147,7 +133,6 @@ export class DriverStateService { .catch(async (e) => { console.log('Error:', e); const content = e instanceof Response ? await e.text() : e; - this._last_error.set(content); notifyError('Failed to recompile driver.', 'View Error', () => this._dialog.open( ViewResponseModalComponent, diff --git a/src/app/drivers/drivers.component.ts b/src/app/drivers/drivers.component.ts index 4e362a831..211d2f432 100644 --- a/src/app/drivers/drivers.component.ts +++ b/src/app/drivers/drivers.component.ts @@ -1,145 +1,44 @@ -import { Component, computed, inject, signal } from '@angular/core'; +import { Component, computed, inject } from '@angular/core'; import { MatRippleModule } from '@angular/material/core'; -import { MatTabNavPanel } from '@angular/material/tabs'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { RouterModule } from '@angular/router'; import { extensionsForItem } from '../common/api'; -import { PlaceDebugService } from '../common/debug.service'; -import { ActiveItemService } from '../common/item.service'; import { i18n } from '../common/locale.service'; -import { DebugOutputComponent } from '../ui/debug-output.component'; import { IconComponent } from '../ui/icon.component'; -import { ItemDetailsSkeletonComponent } from '../ui/item-details-skeleton.component'; -import { ItemDetailsComponent } from '../ui/item-details.component'; -import { ItemSelectionComponent } from '../ui/item-selection.component'; -import { ItemSidebarComponent } from '../ui/item-sidebar.component'; -import { ItemTab, ItemTablistComponent } from '../ui/item-tablist.component'; -import { SidebarMenuComponent } from '../ui/sidebar-menu.component'; +import { ItemShellComponent } from '../ui/item-shell.component'; +import { ItemTab } from '../ui/item-tablist.component'; import { TranslatePipe } from '../ui/translate.pipe'; import { DriverStateService } from './driver-state.service'; @Component({ selector: 'new-drivers-view', template: ` -
- -
-
-
- @if (debug_position() === 'below') { - - } -
- @if (debug_position() === 'side') { - + + @if (updates_available()) { + } -
+ `, - styles: [``], imports: [ - MatTabNavPanel, - DebugOutputComponent, + ItemShellComponent, IconComponent, - TranslatePipe, MatRippleModule, MatTooltipModule, - ItemDetailsComponent, - ItemDetailsSkeletonComponent, - ItemTablistComponent, - ItemSelectionComponent, - ItemSidebarComponent, - SidebarMenuComponent, - RouterModule, + TranslatePipe, ], }) export class DriversComponent { - protected _service = inject(ActiveItemService); private _drivers = inject(DriverStateService); - private _debug = inject(PlaceDebugService); public readonly name = 'drivers'; public readonly item = this._drivers.item; - public readonly loading = this._service.loading; - public readonly scroll = signal(0); - public readonly open_menu = signal(false); public readonly device_count = this._drivers.modules_total; public readonly docs = this._drivers.docs; public readonly extensions = computed(() => @@ -177,8 +76,6 @@ export class DriversComponent { ).concat(this.extensions()), ); public readonly updates_available = this._drivers.updates_available; - public readonly debug_position = this._debug.position; public readonly showUpdateList = () => this._drivers.showUpdateList(); - public readonly newItem = () => this._service.create(); } diff --git a/src/app/groups/group-about.component.ts b/src/app/groups/group-about.component.ts index 456b6cd1d..b14288d5a 100644 --- a/src/app/groups/group-about.component.ts +++ b/src/app/groups/group-about.component.ts @@ -8,7 +8,6 @@ import { showDomain, showGroup, } from '@placeos/ts-client'; -import { toSignal } from '../common/signals'; import { DateFromPipe } from '../ui/pipes/date-from.pipe'; import { MarkdownPipe } from '../ui/pipes/markdown.pipe'; import { TranslatePipe } from '../ui/translate.pipe'; @@ -193,9 +192,7 @@ import { GroupStateService } from './group-state.service'; export class GroupAboutComponent { private _service = inject(GroupStateService); - public readonly item = toSignal(this._service.item, { - initialValue: null as PlaceGroup | null, - }); + public readonly item = this._service.item; public readonly parent = signal(null); public readonly authority = signal(null); public readonly permissionLabels = groupPermissionLabels; diff --git a/src/app/groups/group-bulk-add-modal.component.ts b/src/app/groups/group-bulk-add-modal.component.ts index 2ad5f86af..2648e229e 100644 --- a/src/app/groups/group-bulk-add-modal.component.ts +++ b/src/app/groups/group-bulk-add-modal.component.ts @@ -254,7 +254,6 @@ export class GroupBulkAddModalComponent { public readonly show_permissions = !!this._data.show_permissions; public readonly permission_flags = GROUP_PERMISSION_FLAGS; public readonly hasPermission = hasGroupPermission; - public readonly selected_count = computed(() => this.selected().length); private readonly _items = resource({ params: () => this.search().trim(), loader: async ({ params }) => { diff --git a/src/app/groups/group-form.component.ts b/src/app/groups/group-form.component.ts index 51688bef9..57a9b0576 100644 --- a/src/app/groups/group-form.component.ts +++ b/src/app/groups/group-form.component.ts @@ -10,7 +10,7 @@ import { signal, } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { disabled, form, FormField, submit } from '@angular/forms/signals'; +import { disabled, form, FormField } from '@angular/forms/signals'; import { MatChipInputEvent, MatChipsModule } from '@angular/material/chips'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; @@ -26,18 +26,15 @@ import { showGroup, updateGroup, } from '@placeos/ts-client'; -import { AsyncHandler } from '../common/async-handler.class'; -import { readError } from '../common/errors'; import { addSignalChipItem, - getInvalidSignalFields, + onSaveHotkey, removeSignalChipItem, + submitFormDialog, } from '../common/forms'; import { subtreeFilter } from '../common/hierarchy'; -import { HotkeysService } from '../common/hotkeys.service'; import { i18n } from '../common/locale.service'; -import { notifyError, notifySuccess } from '../common/notifications'; -import { DialogEvent, Identity } from '../common/types'; +import { DialogEvent } from '../common/types'; import { ItemSearchFieldComponent } from '../ui/custom-fields/item-search-field.component'; import { FullscreenModalShellComponent } from '../ui/fullscreen-modal-shell.component'; import { IconComponent } from '../ui/icon.component'; @@ -250,11 +247,10 @@ import { SettingsToggleComponent, ], }) -export class GroupFormComponent extends AsyncHandler implements OnInit { +export class GroupFormComponent implements OnInit { private _dialog_ref = inject>(MatDialogRef); private _data = inject<{ item: PlaceGroup }>(MAT_DIALOG_DATA); - private _hotkey = inject(HotkeysService); private readonly _name = 'GROUPS'; @Output() public event = new EventEmitter(); @@ -310,11 +306,11 @@ export class GroupFormComponent extends AsyncHandler implements OnInit { public readonly exclude_parent_group = (group: PlaceGroup, __: string) => group.id === this._data.item.id; + constructor() { + onSaveHotkey(() => this.submit()); + } + public ngOnInit(): void { - this.subscription( - 'save_item_key', - this._hotkey.listen(['KeyS'], () => this.submit()), - ); this.loadParentGroup(); } @@ -361,44 +357,29 @@ export class GroupFormComponent extends AsyncHandler implements OnInit { })); public async submit(): Promise { - await submit(this.form, async () => { - const item = this._data.item; - this.loading.set(i18n(`${this._name}.SAVING`)); - const form_value = this.formModel(); - const form_item = cleanObject( - { - ...item, - ...form_value, - subsystems: form_value.subsystems || [], - }, - ['', undefined], - ) as Identity; - try { - const _item = await (form_item.id - ? updateGroup( - form_item.id as string, - form_item as unknown as PlaceGroup, - ) - : addGroup(form_item as unknown as PlaceGroup)); - this.event.emit({ reason: 'done', metadata: { item: _item } }); - notifySuccess(i18n(`${this._name}.SAVE_SUCCESS`)); - this._dialog_ref.close(); - } catch (err) { - this.loading.set(null); - notifyError( - i18n(`${this._name}.SAVE_ERROR`, { - error: await readError(err), - }), - ); - } - }); - if (this.form().invalid()) { - return notifyError( - i18n('COMMON.INVALID_FIELDS', { - field_list: getInvalidSignalFields(this.form).join(', '), - }), - ); - } + await submitFormDialog( + this.form, + { + loading: this.loading, + event: this.event, + close: () => this._dialog_ref.close(), + }, + `${this._name}.`, + () => { + const form_value = this.formModel(); + const form_item = cleanObject( + { + ...this._data.item, + ...form_value, + subsystems: form_value.subsystems || [], + }, + ['', undefined], + ) as unknown as PlaceGroup; + return form_item.id + ? updateGroup(form_item.id, form_item) + : addGroup(form_item); + }, + ); } private async loadParentGroup() { diff --git a/src/app/groups/group-membership.component.ts b/src/app/groups/group-membership.component.ts new file mode 100644 index 000000000..d0890d5d0 --- /dev/null +++ b/src/app/groups/group-membership.component.ts @@ -0,0 +1,213 @@ +import { + Component, + computed, + input, + TemplateRef, + viewChild, +} from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { MatRippleModule } from '@angular/material/core'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { RouterModule } from '@angular/router'; +import { i18n } from '../common/locale.service'; +import { ItemSearchFieldComponent } from '../ui/custom-fields/item-search-field.component'; +import { IconComponent } from '../ui/icon.component'; +import { + SimpleTableComponent, + TableColumn, +} from '../ui/simple-table.component'; +import { TranslatePipe } from '../ui/translate.pipe'; +import { GroupMembership } from './group-membership'; +import { groupPermissionLabels } from './group-permissions'; + +/** Tab content that lists, adds and edits one kind of group membership */ +@Component({ + selector: 'group-membership', + template: ` + @let opts = members().options; +
+
+ + +
+
+ + +
+
+ +
+ + {{ opts.member_name(row) }} + +
+ {{ opts.member_detail(row) }} +
+
+
+ +
+ @let labels = permissionLabels(row.permissions); + @for (label of labels; track label) { + + {{ label | translate }} + + } + @if (!labels.length) { + {{ + opts.no_permissions | translate + }} + } +
+
+ +
+ + {{ + (row.deny ? 'COMMON.TRUE' : 'COMMON.FALSE') | translate + }} + +
+
+ +
+ + +
+
+ `, + styles: [ + ` + :host { + display: block; + height: 100%; + width: 100%; + } + `, + ], + imports: [ + FormsModule, + IconComponent, + ItemSearchFieldComponent, + MatProgressBarModule, + MatRippleModule, + MatTooltipModule, + RouterModule, + SimpleTableComponent, + TranslatePipe, + ], +}) +export class GroupMembershipComponent { + public readonly members = input.required(); + /** Whether the membership rows are loading or saving */ + public readonly loading = input(false); + + public readonly permissionLabels = groupPermissionLabels; + public readonly rows = computed(() => this.members().options.rows()); + private readonly _member = + viewChild>('member_template'); + private readonly _permissions = viewChild>( + 'permissions_template', + ); + private readonly _deny = viewChild>('deny_template'); + private readonly _actions = + viewChild>('actions_template'); + + public readonly columns = computed(() => { + const opts = this.members().options; + return [ + { + key: opts.member_key, + name: i18n(opts.column), + content: this._member(), + }, + { + key: 'permissions', + name: i18n('GROUPS.FIELD_PERMISSIONS'), + content: this._permissions(), + size: '24rem', + }, + { + key: 'deny', + name: i18n('GROUPS.FIELD_DENY'), + content: this._deny(), + size: '6rem', + show: !!opts.show_deny, + }, + { + key: 'actions', + name: i18n('COMMON.ACTIONS'), + hide_name: true, + size: '7rem', + content: this._actions(), + }, + ]; + }); + public readonly query_fn = (q: string) => + this.members().options.query(q, this.members().options.authority_id()); + public readonly exclude_fn = (item: { id: string; name: string }) => + this.members().exclude(item); +} diff --git a/src/app/groups/group-membership.ts b/src/app/groups/group-membership.ts new file mode 100644 index 000000000..9d12f0ed5 --- /dev/null +++ b/src/app/groups/group-membership.ts @@ -0,0 +1,216 @@ +import { signal } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { describeError } from '../common/errors'; +import { escapeHtml } from '../common/general'; +import { i18n } from '../common/locale.service'; +import { notifyError, notifySuccess } from '../common/notifications'; +import { waitForEvent } from '../common/signals'; +import { openConfirmModal } from '../overlays/confirm-modal.component'; +import { + GroupBulkAddItem, + GroupBulkAddModalComponent, + GroupBulkAddModalData, + GroupBulkAddResult, +} from './group-bulk-add-modal.component'; +import { GroupPermissionsModalComponent } from './group-permissions-modal.component'; + +/** Settings for one kind of group membership, e.g. the users of a group */ +export interface GroupMembershipOptions { + /** Locale key prefix for one member, e.g. `GROUPS.USER` */ + one: string; + /** Locale key prefix for many members, e.g. `GROUPS.USERS` */ + many: string; + /** Locale key for the search placeholder */ + search: string; + /** Locale key for the member column name */ + column: string; + /** Field of a row that holds the member item, e.g. `user`. Sorts the column. */ + member_key: string; + /** Route of the member items, e.g. `users` */ + route: string; + /** Locale key to show when a row has no permissions */ + no_permissions: string; + /** Show and edit the deny flag of each row */ + show_deny?: boolean; + /** Current membership rows */ + rows: () => Row[]; + /** Domain that members must belong to */ + authority_id: () => string | undefined; + /** Find items that can become members */ + query: (q: string, authority_id: string | undefined) => Promise; + /** ID of the member item of a row */ + member_id: (row: Row) => string; + /** Display name of the member item of a row */ + member_name: (row: Row) => string; + /** Second line of text for the member item of a row */ + member_detail: (row: Row) => string; + /** Domain of a search result. Results from another domain are excluded. */ + item_authority: (item: Item) => string | undefined; + /** Start value for bulk add permissions. Shows the permissions field. */ + bulk_permissions?: () => number; + add: (id: string, permissions?: number) => Promise; + remove: (row: Row) => Promise; + update: ( + row: Row, + changes: { permissions: number; deny: boolean }, + ) => Promise; + /** Reload the membership rows */ + changed: () => void; +} + +export type GroupMembership< + Row extends { permissions?: number; deny?: boolean } = { + permissions?: number; + deny?: boolean; + }, + Item extends GroupBulkAddItem = GroupBulkAddItem, +> = ReturnType>; + +/** + * Add, bulk add, remove and edit actions for one kind of group membership. + * State services create one for each membership list they own. + */ +export function groupMembership< + Row extends { permissions?: number; deny?: boolean }, + Item extends GroupBulkAddItem, +>(dialog: MatDialog, options: GroupMembershipOptions) { + const { one, many } = options; + const saving = signal(false); + + /** Whether an item is already a member or is in another domain */ + const exclude = (item: Item) => { + const authority_id = options.authority_id(); + const item_authority = options.item_authority(item); + return ( + options.rows().some((row) => options.member_id(row) === item.id) || + (!!authority_id && + !!item_authority && + item_authority !== authority_id) + ); + }; + + const add = async (item: Item) => { + if (!item?.id) return; + try { + await options.add(item.id); + } catch (error) { + return notifyError( + i18n(`${one}_ADD_ERROR`, { error: describeError(error) }), + ); + } + notifySuccess(i18n(`${one}_ADD_SUCCESS`)); + options.changed(); + }; + + const bulkAdd = async () => { + const show_permissions = !!options.bulk_permissions; + const result = await waitForEvent( + dialog + .open< + GroupBulkAddModalComponent, + GroupBulkAddModalData, + GroupBulkAddResult + >(GroupBulkAddModalComponent, { + data: { + title: `${many}_BULK`, + placeholder: options.search, + empty_message: `${many}_BULK_EMPTY`, + show_permissions, + permissions: options.bulk_permissions?.(), + query_fn: (q: string) => + options.query(q, options.authority_id()), + exclude, + }, + panelClass: 'bulk-add-dialog', + height: 'auto', + width: 'auto', + maxHeight: 'calc(100vh - 2em)', + maxWidth: 'calc(100vw - 2em)', + }) + .afterClosed(), + ); + const items = result?.items; + if (!items?.length) return; + const permissions = show_permissions + ? +result.permissions || 0 + : undefined; + saving.set(true); + const results = await Promise.allSettled( + items.map((item) => options.add(item.id, permissions)), + ); + saving.set(false); + const failed = results.filter((_) => _.status === 'rejected').length; + if (failed) { + notifyError(i18n(`${many}_BULK_ERROR`, { count: failed })); + } + const added = results.length - failed; + if (added) { + notifySuccess(i18n(`${many}_BULK_SUCCESS`, { count: added })); + } + options.changed(); + }; + + const remove = async (row: Row) => { + const details = await openConfirmModal( + { + title: i18n(`${one}_REMOVE`), + content: i18n(`${one}_REMOVE_MSG`, { + name: escapeHtml(options.member_name(row)), + }), + icon: { type: 'icon', content: 'delete' }, + }, + dialog, + ); + if (details.reason !== 'done') return; + details.loading(i18n(`${one}_REMOVE_LOADING`)); + try { + await options.remove(row); + } catch (error) { + details.close(); + return notifyError( + i18n(`${one}_REMOVE_ERROR`, { error: describeError(error) }), + ); + } + details.close(); + notifySuccess(i18n(`${one}_REMOVE_SUCCESS`)); + options.changed(); + }; + + const editPermissions = async (row: Row) => { + const result = await waitForEvent( + dialog + .open(GroupPermissionsModalComponent, { + data: { + title: `${one}_PERMISSIONS`, + permissions: row.permissions, + deny: row.deny, + show_deny: !!options.show_deny, + }, + }) + .afterClosed(), + ); + if (!result) return; + try { + await options.update(row, { + permissions: +result.permissions || 0, + deny: !!result.deny, + }); + } catch (error) { + return notifyError( + i18n(`${one}_SAVE_ERROR`, { error: describeError(error) }), + ); + } + notifySuccess(i18n(`${one}_SAVE_SUCCESS`)); + options.changed(); + }; + + return { + options, + saving: saving.asReadonly(), + exclude, + add, + bulkAdd, + remove, + editPermissions, + }; +} diff --git a/src/app/groups/group-state.service.ts b/src/app/groups/group-state.service.ts index dcc1ee91c..06b4b1d39 100644 --- a/src/app/groups/group-state.service.ts +++ b/src/app/groups/group-state.service.ts @@ -18,30 +18,25 @@ import { updateGroupZone, } from '@placeos/ts-client'; import { describeError } from '../common/errors'; -import { escapeHtml } from '../common/general'; import { ActiveItemService } from '../common/item.service'; import { i18n } from '../common/locale.service'; -import { notifyError, notifySuccess } from '../common/notifications'; +import { notifyError } from '../common/notifications'; import { openedTabs } from '../common/opened-tabs'; -import { waitForEvent } from '../common/signals'; -import { openConfirmModal } from '../overlays/confirm-modal.component'; -import { GroupBulkAddModalComponent } from './group-bulk-add-modal.component'; -import { GroupPermissionsModalComponent } from './group-permissions-modal.component'; +import { groupMembership } from './group-membership'; @Service() export class GroupStateService { private _state = inject(ActiveItemService); private _dialog = inject(MatDialog); private _changed = signal(0); - /** Set while a bulk add runs */ - private _saving = signal(false); public readonly item = computed( () => this._state.item() as unknown as PlaceGroup, ); public readonly loading = computed( () => - this._saving() || + this.user_members.saving() || + this.zone_members.saving() || this._users.isLoading() || this._zones.isLoading(), ); @@ -141,280 +136,76 @@ export class GroupStateService { return this._state.active_item as unknown as PlaceGroup; } - public async addUser(user: PlaceUser) { - if (!user?.id) return; - try { - await addGroupUser({ - group_id: this.active_item.id, - user_id: user.id, - }); - } catch (error) { - return notifyError( - i18n('GROUPS.USER_ADD_ERROR', { error: describeError(error) }), - ); - } - notifySuccess(i18n('GROUPS.USER_ADD_SUCCESS')); - this.changed(); - } - - public async bulkAddUsers(existing_users: PlaceGroupUser[] = []) { - const result = await waitForEvent( - this._dialog - .open(GroupBulkAddModalComponent, { - data: { - title: 'GROUPS.USERS_BULK', - placeholder: 'GROUPS.USER_SEARCH', - empty_message: 'GROUPS.USERS_BULK_EMPTY', - show_permissions: true, - permissions: this.active_item?.default_permissions, - query_fn: (query: string) => - queryUsers({ - q: query, - limit: 20, - authority_id: this.active_item?.authority_id, - }).then((response) => response.data), - exclude: (user: PlaceUser) => { - const authority_id = this.active_item?.authority_id; - return ( - !!existing_users.find( - (_) => _.user_id === user.id, - ) || - (!!authority_id && - user.authority_id !== authority_id) - ); - }, - }, - panelClass: 'bulk-add-dialog', - height: 'auto', - width: 'auto', - maxHeight: 'calc(100vh - 2em)', - maxWidth: 'calc(100vw - 2em)', - }) - .afterClosed(), - ); - const users = result?.items; - if (!users?.length) return; - const permissions = +result.permissions || 0; - this._saving.set(true); - const results = await Promise.allSettled( - users.map((user) => + /** User memberships of the active group */ + public readonly user_members = groupMembership( + this._dialog, + { + one: 'GROUPS.USER', + many: 'GROUPS.USERS', + search: 'GROUPS.USER_SEARCH', + column: 'GROUPS.FIELD_USER', + member_key: 'user', + route: 'users', + no_permissions: 'COMMON.NONE', + rows: () => this.users(), + authority_id: () => this.active_item?.authority_id, + query: (q, authority_id) => + queryUsers({ q, limit: 20, authority_id }).then( + (response) => response.data, + ), + member_id: (row) => row.user_id, + member_name: (row) => row.user?.name || row.user_id, + member_detail: (row) => row.user?.email, + item_authority: (user) => user.authority_id, + bulk_permissions: () => this.active_item?.default_permissions, + add: (user_id, permissions) => addGroupUser({ group_id: this.active_item.id, - user_id: user.id, - permissions, - }), - ), - ); - this._saving.set(false); - const failed = results.filter((_) => _.status === 'rejected').length; - if (failed) { - notifyError(i18n('GROUPS.USERS_BULK_ERROR', { count: failed })); - } - const added = results.length - failed; - if (added) { - notifySuccess(i18n('GROUPS.USERS_BULK_SUCCESS', { count: added })); - } - this.changed(); - } - - public async bulkAddZones(existing_zones: PlaceGroupZone[] = []) { - const result = await waitForEvent( - this._dialog - .open(GroupBulkAddModalComponent, { - data: { - title: 'GROUPS.ZONES_BULK', - placeholder: 'GROUPS.ZONE_SEARCH', - empty_message: 'GROUPS.ZONES_BULK_EMPTY', - query_fn: (query: string) => - queryZones({ - q: query, - limit: 20, - authority_id: this.active_item?.authority_id, - } as Record).then( - (response) => response.data, - ), - exclude: (zone: PlaceZone) => { - const authority_id = this.active_item?.authority_id; - const zone_authority_id = ( - zone as PlaceZone & { authority_id?: string } - ).authority_id; - return ( - !!existing_zones.find( - (_) => _.zone_id === zone.id, - ) || - (!!authority_id && - !!zone_authority_id && - zone_authority_id !== authority_id) - ); - }, - }, - panelClass: 'bulk-add-dialog', - height: 'auto', - width: 'auto', - maxHeight: 'calc(100vh - 2em)', - maxWidth: 'calc(100vw - 2em)', - }) - .afterClosed(), - ); - const zones = result?.items; - if (!zones?.length) return; - this._saving.set(true); - const results = await Promise.allSettled( - zones.map((zone) => - addGroupZone({ - group_id: this.active_item.id, - zone_id: zone.id, + user_id, + ...(permissions === undefined ? {} : { permissions }), }), - ), - ); - this._saving.set(false); - const failed = results.filter((_) => _.status === 'rejected').length; - if (failed) { - notifyError(i18n('GROUPS.ZONES_BULK_ERROR', { count: failed })); - } - const added = results.length - failed; - if (added) { - notifySuccess(i18n('GROUPS.ZONES_BULK_SUCCESS', { count: added })); - } - this.changed(); - } - - public async removeUser(item: PlaceGroupUser) { - const details = await openConfirmModal( - { - title: i18n('GROUPS.USER_REMOVE'), - content: i18n('GROUPS.USER_REMOVE_MSG', { - name: escapeHtml(item.user?.name || item.user_id), - }), - icon: { type: 'icon', content: 'delete' }, - }, - this._dialog, - ); - if (details.reason !== 'done') return; - details.loading(i18n('GROUPS.USER_REMOVE_LOADING')); - try { - await removeGroupUser(item.user_id, item.group_id); - } catch (error) { - details.close(); - return notifyError( - i18n('GROUPS.USER_REMOVE_ERROR', { - error: describeError(error), - }), - ); - } - details.close(); - notifySuccess(i18n('GROUPS.USER_REMOVE_SUCCESS')); - this.changed(); - } - - public async updateUser(item: PlaceGroupUser) { - try { - await updateGroupUser(item.user_id, item.group_id, { - permissions: +item.permissions || 0, - }); - } catch (error) { - return notifyError( - i18n('GROUPS.USER_SAVE_ERROR', { error: describeError(error) }), - ); - } - notifySuccess(i18n('GROUPS.USER_SAVE_SUCCESS')); - this.changed(); - } - - public async editUserPermissions(item: PlaceGroupUser) { - const result = await waitForEvent( - this._dialog - .open(GroupPermissionsModalComponent, { - data: { - title: 'GROUPS.USER_PERMISSIONS', - permissions: item.permissions, - }, - }) - .afterClosed(), - ); - if (!result) return; - await this.updateUser({ ...item, permissions: result.permissions }); - } - - public async addZone(zone: PlaceZone) { - if (!zone?.id) return; - try { - await addGroupZone({ - group_id: this.active_item.id, - zone_id: zone.id, - }); - } catch (error) { - return notifyError( - i18n('GROUPS.ZONE_ADD_ERROR', { error: describeError(error) }), - ); - } - notifySuccess(i18n('GROUPS.ZONE_ADD_SUCCESS')); - this.changed(); - } + remove: (row) => removeGroupUser(row.user_id, row.group_id), + update: (row, { permissions }) => + updateGroupUser(row.user_id, row.group_id, { permissions }), + changed: () => this.changed(), + }, + ); - public async removeZone(item: PlaceGroupZone) { - const details = await openConfirmModal( - { - title: i18n('GROUPS.ZONE_REMOVE'), - content: i18n('GROUPS.ZONE_REMOVE_MSG', { - name: escapeHtml(item.zone?.name || item.zone_id), - }), - icon: { type: 'icon', content: 'delete' }, - }, - this._dialog, - ); - if (details.reason !== 'done') return; - details.loading(i18n('GROUPS.ZONE_REMOVE_LOADING')); - try { - await removeGroupZone(item.group_id, item.zone_id); - } catch (error) { - details.close(); - return notifyError( - i18n('GROUPS.ZONE_REMOVE_ERROR', { - error: describeError(error), + /** Zone memberships of the active group */ + public readonly zone_members = groupMembership( + this._dialog, + { + one: 'GROUPS.ZONE', + many: 'GROUPS.ZONES', + search: 'GROUPS.ZONE_SEARCH', + column: 'GROUPS.FIELD_ZONE', + member_key: 'zone', + route: 'zones', + no_permissions: 'COMMON.DEFAULT', + show_deny: true, + rows: () => this.zones(), + authority_id: () => this.active_item?.authority_id, + query: (q, authority_id) => + queryZones({ q, limit: 20, authority_id } as Record< + string, + unknown + >).then((response) => response.data), + member_id: (row) => row.zone_id, + member_name: (row) => row.zone?.name || row.zone_id, + member_detail: (row) => row.zone_id, + item_authority: (zone) => + (zone as PlaceZone & { authority_id?: string }).authority_id, + add: (zone_id) => + addGroupZone({ group_id: this.active_item.id, zone_id }), + remove: (row) => removeGroupZone(row.group_id, row.zone_id), + update: (row, { permissions, deny }) => + updateGroupZone(row.group_id, row.zone_id, { + permissions, + deny, }), - ); - } - details.close(); - notifySuccess(i18n('GROUPS.ZONE_REMOVE_SUCCESS')); - this.changed(); - } - - public async updateZone(item: PlaceGroupZone) { - try { - await updateGroupZone(item.group_id, item.zone_id, { - permissions: +item.permissions || 0, - deny: !!item.deny, - }); - } catch (error) { - return notifyError( - i18n('GROUPS.ZONE_SAVE_ERROR', { error: describeError(error) }), - ); - } - notifySuccess(i18n('GROUPS.ZONE_SAVE_SUCCESS')); - this.changed(); - } - - public async editZonePermissions(item: PlaceGroupZone) { - const result = await waitForEvent( - this._dialog - .open(GroupPermissionsModalComponent, { - data: { - title: 'GROUPS.ZONE_PERMISSIONS', - permissions: item.permissions, - deny: item.deny, - show_deny: true, - }, - }) - .afterClosed(), - ); - if (!result) return; - await this.updateZone({ - ...item, - permissions: result.permissions, - deny: result.deny, - }); - } + changed: () => this.changed(), + }, + ); private changed() { this._changed.set(new Date().valueOf()); diff --git a/src/app/groups/group-users.component.ts b/src/app/groups/group-users.component.ts index 522c21a9a..c058ce1c8 100644 --- a/src/app/groups/group-users.component.ts +++ b/src/app/groups/group-users.component.ts @@ -1,181 +1,17 @@ -import { Component, effect, inject } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { MatRippleModule } from '@angular/material/core'; -import { MatProgressBarModule } from '@angular/material/progress-bar'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { RouterModule } from '@angular/router'; -import { PlaceGroupUser, PlaceUser, queryUsers } from '@placeos/ts-client'; -import { ItemSearchFieldComponent } from '../ui/custom-fields/item-search-field.component'; -import { IconComponent } from '../ui/icon.component'; -import { LoadingLabelDirective } from '../ui/loading-label.directive'; -import { SimpleTableComponent } from '../ui/simple-table.component'; -import { TranslatePipe } from '../ui/translate.pipe'; -import { groupPermissionLabels } from './group-permissions'; +import { Component, inject } from '@angular/core'; +import { GroupMembershipComponent } from './group-membership.component'; import { GroupStateService } from './group-state.service'; @Component({ selector: 'group-users', template: ` -
-
- - -
-
- - -
-
- -
- - {{ row.user?.name || row.user_id }} - -
{{ row.user?.email }}
-
-
- -
- @let labels = permissionLabels(row.permissions); - @for (label of labels; track label) { - - {{ label | translate }} - - } - @if (!labels.length) { - {{ - 'COMMON.NONE' | translate - }} - } -
-
- -
- - -
-
+ `, - styles: [ - ` - :host { - height: 100%; - width: 100%; - } - `, - ], - imports: [ - LoadingLabelDirective, - FormsModule, - IconComponent, - ItemSearchFieldComponent, - MatProgressBarModule, - MatRippleModule, - MatTooltipModule, - RouterModule, - SimpleTableComponent, - TranslatePipe, - ], + imports: [GroupMembershipComponent], }) export class GroupUsersComponent { private _service = inject(GroupStateService); - public readonly users = this._service.users; + public readonly members = this._service.user_members; public readonly loading = this._service.loading; - public readonly permissionLabels = groupPermissionLabels; - public readonly addUser = (user: PlaceUser) => this._service.addUser(user); - public readonly bulkAddUsers = () => - this._service.bulkAddUsers(this._current_users); - public readonly removeUser = (row: PlaceGroupUser) => - this._service.removeUser(row); - public readonly editPermissions = (row: PlaceGroupUser) => - this._service.editUserPermissions(row); - public readonly query_fn = (_: string) => - queryUsers({ - q: _, - authority_id: this._service.active_item?.authority_id, - }).then((resp) => resp.data); - public readonly exclude_fn = (user: PlaceUser, __: string) => { - const authority_id = this._service.active_item?.authority_id; - return ( - !!this._service.active_item && - (!!this._current_users.find((_) => _.user_id === user.id) || - (!!authority_id && user.authority_id !== authority_id)) - ); - }; - - private _current_users: PlaceGroupUser[] = []; - - constructor() { - effect(() => (this._current_users = this.users())); - } } diff --git a/src/app/groups/group-zones.component.ts b/src/app/groups/group-zones.component.ts index 1b8cbfa55..15f615152 100644 --- a/src/app/groups/group-zones.component.ts +++ b/src/app/groups/group-zones.component.ts @@ -1,207 +1,17 @@ -import { Component, effect, inject } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { MatRippleModule } from '@angular/material/core'; -import { MatProgressBarModule } from '@angular/material/progress-bar'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { RouterModule } from '@angular/router'; -import { PlaceGroupZone, PlaceZone, queryZones } from '@placeos/ts-client'; -import { ItemSearchFieldComponent } from '../ui/custom-fields/item-search-field.component'; -import { IconComponent } from '../ui/icon.component'; -import { LoadingLabelDirective } from '../ui/loading-label.directive'; -import { SimpleTableComponent } from '../ui/simple-table.component'; -import { TranslatePipe } from '../ui/translate.pipe'; -import { groupPermissionLabels } from './group-permissions'; +import { Component, inject } from '@angular/core'; +import { GroupMembershipComponent } from './group-membership.component'; import { GroupStateService } from './group-state.service'; @Component({ selector: 'group-zones', template: ` -
-
- - -
-
- - -
-
- - - - -
- @let labels = permissionLabels(row.permissions); - @for (label of labels; track label) { - - {{ label | translate }} - - } - @if (!labels.length) { - {{ - 'COMMON.DEFAULT' | translate - }} - } -
-
- -
- - {{ - (row.deny ? 'COMMON.TRUE' : 'COMMON.FALSE') | translate - }} - -
-
- -
- - -
-
+ `, - styles: [ - ` - :host { - height: 100%; - width: 100%; - } - `, - ], - imports: [ - LoadingLabelDirective, - FormsModule, - IconComponent, - ItemSearchFieldComponent, - MatProgressBarModule, - MatRippleModule, - MatTooltipModule, - RouterModule, - SimpleTableComponent, - TranslatePipe, - ], + imports: [GroupMembershipComponent], }) export class GroupZonesComponent { private _service = inject(GroupStateService); - public readonly zones = this._service.zones; + public readonly members = this._service.zone_members; public readonly loading = this._service.loading; - public readonly permissionLabels = groupPermissionLabels; - public readonly addZone = (zone: PlaceZone) => this._service.addZone(zone); - public readonly bulkAddZones = () => - this._service.bulkAddZones(this._current_zones); - public readonly removeZone = (row: PlaceGroupZone) => - this._service.removeZone(row); - public readonly editPermissions = (row: PlaceGroupZone) => - this._service.editZonePermissions(row); - public readonly query_fn = (_: string) => - queryZones({ - q: _, - authority_id: this._service.active_item?.authority_id, - } as Record).then((resp) => resp.data); - public readonly exclude_fn = (zone: PlaceZone, __: string) => { - const authority_id = this._service.active_item?.authority_id; - const zone_authority_id = ( - zone as PlaceZone & { authority_id?: string } - ).authority_id; - return ( - !!this._service.active_item && - (!!this._current_zones.find((_) => _.zone_id === zone.id) || - (!!authority_id && - !!zone_authority_id && - zone_authority_id !== authority_id)) - ); - }; - - private _current_zones: PlaceGroupZone[] = []; - - constructor() { - effect(() => (this._current_zones = this.zones())); - } } diff --git a/src/app/groups/groups.component.ts b/src/app/groups/groups.component.ts index 5bcbff46b..293b964ed 100644 --- a/src/app/groups/groups.component.ts +++ b/src/app/groups/groups.component.ts @@ -1,119 +1,24 @@ -import { Component, computed, inject, signal } from '@angular/core'; -import { MatRipple } from '@angular/material/core'; -import { MatTabNavPanel } from '@angular/material/tabs'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { RouterModule } from '@angular/router'; -import { AsyncHandler } from '../common/async-handler.class'; -import { ActiveItemService } from '../common/item.service'; +import { Component, computed, inject } from '@angular/core'; import { i18n } from '../common/locale.service'; -import { IconComponent } from '../ui/icon.component'; -import { ItemDetailsSkeletonComponent } from '../ui/item-details-skeleton.component'; -import { - DisplayItem, - ItemDetailsComponent, -} from '../ui/item-details.component'; -import { ItemSelectionComponent } from '../ui/item-selection.component'; -import { ItemSidebarComponent } from '../ui/item-sidebar.component'; -import { ItemTablistComponent } from '../ui/item-tablist.component'; -import { SidebarMenuComponent } from '../ui/sidebar-menu.component'; -import { TranslatePipe } from '../ui/translate.pipe'; +import { DisplayItem } from '../ui/item-details.component'; +import { ItemShellComponent } from '../ui/item-shell.component'; import { GroupStateService } from './group-state.service'; @Component({ selector: 'new-groups-view', template: ` -
- -
+ `, - styles: [``], - imports: [ - MatTabNavPanel, - IconComponent, - MatTooltipModule, - MatRipple, - TranslatePipe, - RouterModule, - ItemTablistComponent, - ItemDetailsComponent, - ItemDetailsSkeletonComponent, - ItemSelectionComponent, - ItemSidebarComponent, - SidebarMenuComponent, - ], + imports: [ItemShellComponent], }) -export class GroupsComponent extends AsyncHandler { +export class GroupsComponent { private _service = inject(GroupStateService); - protected _item = inject(ActiveItemService); - public readonly name = 'groups'; - public open_menu = false; - public readonly scroll = signal(0); - public readonly loading = this._item.loading; public readonly item = this._service.item; public readonly display_item = computed(() => { const item = this.item(); @@ -143,6 +48,4 @@ export class GroupsComponent extends AsyncHandler { }, ]; }); - - public readonly newItem = () => this._item.create(); } diff --git a/src/app/mocks/index.ts b/src/app/mocks/index.ts index ce1a80afd..781ab98c8 100644 --- a/src/app/mocks/index.ts +++ b/src/app/mocks/index.ts @@ -1,4 +1 @@ import './backend'; -import './system.mockup'; - -export * from './system.mockup'; diff --git a/src/app/mocks/system.mockup.ts b/src/app/mocks/system.mockup.ts deleted file mode 100644 index 5ad250f2a..000000000 --- a/src/app/mocks/system.mockup.ts +++ /dev/null @@ -1,112 +0,0 @@ -/** - * @Author: Alex Sorafumo - * @Date: 11/01/2017 4:16 PM - * @Email: alex@yuion.net - * @Filename: mock-system.ts - * @Last modified by: Alex Sorafumo - * @Last modified time: 03/02/2017 2:26 PM - */ - -import { addMinutes, startOfMinute } from 'date-fns'; - -interface MockBackend { - model?: { - user?: { - name: string; - }; - }; -} - -interface MockControlSystem { - [key: string]: unknown; - Slack?: Array<{ - threads: { - local: Array<{ - text: string; - username: string; - ts: number; - }>; - }; - }>; -} - -declare global { - interface Window { - systemData: Record; - control: Record>; - backend: MockBackend; - } -} - -window.systemData = window.systemData || {}; -window.control = window.control || {}; -window.control.systems = window.control.systems || {}; -window.control.systems['sys-B0'] = { - System: [ - { - name: 'Demo System', - }, - ], - Demo: [ - { - volume: 0, - mute: false, - views: 0, - state: 'Idle', - $play() { - this.state = 'Playing'; - }, - $stop() { - this.state = 'Stopped'; - }, - $volume(value: number) { - this.volume = value; - if (this.volume > 100) { - this.volume = 100; - } else if (this.volume < 0) { - this.volume = 0; - } - }, - $mute(state: boolean) { - this.mute = state; - }, - - $state(status: string) { - this.state = status; - }, - }, - ], -}; - -setTimeout(() => initMessages(), 500); - -function initMessages() { - const user = window.backend?.model?.user; - if (user) { - const messages = [ - 'Testing', - 'Response to Testing', - `Hello I'm ${user.name}`, - `Hello ${user.name}, this is the concierge`, - 'Can I book a room for tomorrow at 9:30am?', - 'Sure, how does Activity Space 31.04 sound?', - "That's exactly what I'm looking for, thanks", - 'Alright, you now have a booking for Activity Space 31.04 at 9:30am tomorrow.', - ]; - let time = startOfMinute(addMinutes(Date.now(), -messages.length * 30)); - let index = 0; - for (const msg of messages) { - window.control.systems['sys-B0'].Slack?.[0].threads.local.push({ - text: msg, - username: index % 2 === 0 ? user.name : '', - ts: time.valueOf(), - }); - index++; - time = addMinutes(time, 30); - } - } else { - setTimeout(() => initMessages(), 500); - } -} - -window.systemData['sys-B0'] = window.control.systems['sys-B0']; diff --git a/src/app/modules/module-about.component.ts b/src/app/modules/module-about.component.ts index 63ba89432..f8b58533c 100644 --- a/src/app/modules/module-about.component.ts +++ b/src/app/modules/module-about.component.ts @@ -2,7 +2,6 @@ import { Component, inject, signal } from '@angular/core'; import { PlaceModule } from '@placeos/ts-client'; import { hasSupportRole } from '../common/support-access'; -import { DatePipe } from '@angular/common'; import { MatRippleModule } from '@angular/material/core'; import { MatDialog } from '@angular/material/dialog'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; @@ -11,9 +10,9 @@ import { RouterModule } from '@angular/router'; import { CustomTooltipComponent } from '../ui/custom-tooltip.component'; import { SettingsFormComponent } from '../ui/forms/settings-form.component'; import { IconComponent } from '../ui/icon.component'; +import { ItemTimestampsComponent } from '../ui/item-timestamps.component'; import { LoadingLabelDirective } from '../ui/loading-label.directive'; import { ModuleRuntimeErrorsModalComponent } from '../ui/module-runtime-errors-modal.component'; -import { DateFromPipe } from '../ui/pipes/date-from.pipe'; import { TranslatePipe } from '../ui/translate.pipe'; import { ModuleStateService } from './module-state.service'; @@ -139,38 +138,10 @@ import { ModuleStateService } from './module-state.service';
} -
- {{ 'COMMON.CREATED_AT' | translate }} -
-
- - {{ item.created_at * 1000 | dateFrom }} - -
-
- {{ 'COMMON.UPDATED_AT' | translate }} -
-
- - {{ item.updated_at * 1000 | dateFrom }} - -
+ @if (item.has_runtime_error && hasSupportRole()) { - -
- @if (loading()) { - - } @else if (item()?.id) { - - -
- - - -
- } -
- - -
- @if (debug_position() === 'below') { - - } -
- @if (debug_position() === 'side') { - - } -
+ `, - styles: [``], - imports: [ - MatTabNavPanel, - DebugOutputComponent, - IconComponent, - MatRippleModule, - TranslatePipe, - MatTooltipModule, - RouterModule, - ItemTablistComponent, - ItemDetailsComponent, - ItemDetailsSkeletonComponent, - ItemSelectionComponent, - ItemSidebarComponent, - SidebarMenuComponent, - ], + imports: [ItemShellComponent], }) export class ModulesComponent { private _service = inject(ActiveItemService); - private _debug = inject(PlaceDebugService); private _module_state = inject(ModuleStateService); public readonly name = 'modules'; public readonly item = computed( () => this._service.item() as PlaceModule | null, ); - public readonly loading = this._service.loading; /** Number of systems for the active device */ public readonly system_count = this._module_state.system_total; - public readonly open_menu = signal(false); - public readonly scroll = signal(0); - public readonly debug_position = this._debug.position; - public readonly canCreate = () => this._service.canMutate(2); - public readonly newItem = () => this._service.create(); public readonly extensions = computed(() => extensionsForItem(this.item(), this.name), ); diff --git a/src/app/modules/modules.routes.ts b/src/app/modules/modules.routes.ts index a5e432ade..a8dd507d3 100644 --- a/src/app/modules/modules.routes.ts +++ b/src/app/modules/modules.routes.ts @@ -1,5 +1,5 @@ import { Routes } from '@angular/router'; -import { AuthorisedAdminGuard } from '../ui/guards/authorised-admin.guard'; +import { authorisedAdminGuard } from '../ui/guards/authorised.guard'; export const ROUTES: Routes = [ { @@ -30,7 +30,7 @@ export const ROUTES: Routes = [ }, { path: 'history', - canActivate: [AuthorisedAdminGuard], + canActivate: [authorisedAdminGuard], loadComponent: () => import('../ui/settings-history-view.component').then( (m) => m.SettingsHistoryViewComponent, diff --git a/src/app/overlays/auth-source-modal.component.ts b/src/app/overlays/auth-source-modal.component.ts index 12565cdf1..c37203a6a 100644 --- a/src/app/overlays/auth-source-modal.component.ts +++ b/src/app/overlays/auth-source-modal.component.ts @@ -24,7 +24,6 @@ import { import { MatFormFieldModule } from '@angular/material/form-field'; import { MatSelectModule } from '@angular/material/select'; -import { AsyncHandler } from '../common/async-handler.class'; import { readError } from '../common/errors'; import { i18n } from '../common/locale.service'; import { notifyError, notifySuccess } from '../common/notifications'; @@ -123,7 +122,7 @@ export type AuthSourceTypes = 'oauth' | 'saml' | 'ldap'; OauthSourceFormComponent, ], }) -export class AuthSourceModalComponent extends AsyncHandler implements OnInit { +export class AuthSourceModalComponent implements OnInit { private _dialog = inject>(MatDialogRef); private _data = inject(MAT_DIALOG_DATA); diff --git a/src/app/overlays/bulk-item-modal/bulk-item-modal.component.ts b/src/app/overlays/bulk-item-modal/bulk-item-modal.component.ts index b0a7a7b72..e5d489634 100644 --- a/src/app/overlays/bulk-item-modal/bulk-item-modal.component.ts +++ b/src/app/overlays/bulk-item-modal/bulk-item-modal.component.ts @@ -238,7 +238,7 @@ export class BulkItemModalComponent< const Resource = this._data.constr; this.item_list.set( data.map((item) => { - // Values are already parsed by csvToJson/parseCSV, + // Values are already parsed by parseCSV, // so no need for additional JSON.parse here return new Resource(item); }), diff --git a/src/app/overlays/bulk-item-modal/csv-upload.component.ts b/src/app/overlays/bulk-item-modal/csv-upload.component.ts index c78c50623..5eccb42b4 100644 --- a/src/app/overlays/bulk-item-modal/csv-upload.component.ts +++ b/src/app/overlays/bulk-item-modal/csv-upload.component.ts @@ -2,7 +2,7 @@ import { Component, input, output, signal } from '@angular/core'; import { MatRippleModule } from '@angular/material/core'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { csvToJson, downloadFile, jsonToCsv } from '../../common/general'; +import { downloadFile, jsonToCsv, parseCSV } from '../../common/general'; import { notifyError, notifyWarn } from '../../common/notifications'; import { HashMap } from '../../common/types'; import { IconComponent } from '../../ui/icon.component'; @@ -157,7 +157,7 @@ export class CsvUploadComponent { private processCSVData(data: string, seperator: '\t' | ',' | ' ' = '\t') { try { - const list = csvToJson(data, seperator) || []; + const list = parseCSV(data, seperator) || []; this.loading = false; if (!list.length) { notifyWarn('No rows were found in the uploaded file.'); diff --git a/src/app/overlays/confirm-modal.component.ts b/src/app/overlays/confirm-modal.component.ts index 2b478dd3b..17909b1c1 100644 --- a/src/app/overlays/confirm-modal.component.ts +++ b/src/app/overlays/confirm-modal.component.ts @@ -20,9 +20,13 @@ import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatRippleModule } from '@angular/material/core'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { lastValueFrom } from 'rxjs'; -import { describeError } from '../common/errors'; +import { describeError, readError } from '../common/errors'; import { i18n } from '../common/locale.service'; -import { notifyInfo } from '../common/notifications'; +import { + notifyError, + notifyInfo, + notifySuccess, +} from '../common/notifications'; import { waitForEvent } from '../common/signals'; import { ApplicationIcon, DialogEvent } from '../common/types'; import { DialogTitleDirective } from '../ui/dialog-title.directive'; @@ -175,6 +179,37 @@ export async function openConfirmModal( }; } +/** + * Ask the user to confirm, then run `action` while the modal shows `loading`. + * The modal closes when the action ends. A failed action shows + * `error(reason)`; a successful one shows `success`, if set. + * @returns Whether the action ran and succeeded + */ +export async function confirmAndRun( + dialog: MatDialog, + data: ConfirmModalData, + options: { + loading: string; + action: () => Promise; + error: (reason: string) => string; + success?: string; + }, +): Promise { + const details = await openConfirmModal(data, dialog); + if (details.reason !== 'done') return false; + details.loading(options.loading); + try { + await options.action(); + } catch (err) { + notifyError(options.error(await readError(err))); + return false; + } finally { + details.close(); + } + if (options.success) notifySuccess(options.success); + return true; +} + @Component({ selector: 'confirm-modal', template: ` diff --git a/src/app/overlays/duplicate-modal.component.ts b/src/app/overlays/duplicate-modal.component.ts index 52adf6a08..83814c312 100644 --- a/src/app/overlays/duplicate-modal.component.ts +++ b/src/app/overlays/duplicate-modal.component.ts @@ -20,7 +20,6 @@ import { describeError } from '../common/errors'; import { notifyError } from '../common/notifications'; import { DialogEvent, HashMap } from '../common/types'; import { IconComponent } from '../ui/icon.component'; -import { LoadingLabelDirective } from '../ui/loading-label.directive'; export interface DuplicateModalData { item: HashMap; @@ -35,51 +34,43 @@ export interface DuplicateModalData { @if (!loading()) { -
-
-

- How many times would you like to duplicate this - item? -

- - - -
+
+

How many times would you like to duplicate this item?

+ + +
} @else { -
+
@if (!done()) { -
Creating item duplicates...
+
+ Creating item duplicates... +
} @for (itm of temp(); track $index; let i = $index) { -
-
+
+
{{ item.name }} ({{ i + 1 }})
-
- @if (status()[i] !== 'loading') { - - {{ - status()[i] === 'done' - ? 'done' - : 'close' - }} - - } - @if (status()[i] === 'loading') { - - } -
+ @if (status()[i] === 'loading') { + + } @else if (status()[i] === 'done') { + done + } @else { + close + }
}
@@ -87,10 +78,13 @@ export interface DuplicateModalData { @if (!loading()) { - + - -
- @if (loading()) { - - } @else if (item()?.id) { - - -
- - - -
- } -
- - -
+ `, - styles: [``], - imports: [ - MatTabNavPanel, - IconComponent, - MatRippleModule, - MatTooltipModule, - TranslatePipe, - RouterModule, - ItemTablistComponent, - ItemDetailsComponent, - ItemDetailsSkeletonComponent, - ItemSelectionComponent, - SidebarMenuComponent, - ItemSidebarComponent, - ], + imports: [ItemShellComponent], }) export class RepositoriesComponent { protected _service = inject(RepositoriesStateService); - protected _item = inject(ActiveItemService); public readonly name = 'repositories'; - public readonly open_menu = signal(false); - - public readonly newItem = () => this._item.create(); - public readonly item = this._service.item; - public readonly loading = this._item.loading; public readonly driver_list_loading = this._service.loading; public readonly driver_list = this._service.driver_list; - public readonly driver_list_error = this._service.driver_list_error; public readonly has_changelog = this._service.has_changelog; public readonly driver_count = computed(() => { const list = this.driver_list(); @@ -168,5 +74,4 @@ export class RepositoriesComponent { ]), ...this.extensions(), ]); - public readonly scroll = signal(0); } diff --git a/src/app/repositories/repository-about.component.ts b/src/app/repositories/repository-about.component.ts index 256293419..4ad2b82fa 100644 --- a/src/app/repositories/repository-about.component.ts +++ b/src/app/repositories/repository-about.component.ts @@ -1,12 +1,12 @@ -import { AsyncPipe, DatePipe } from '@angular/common'; +import { AsyncPipe } from '@angular/common'; import { Component, computed, inject, signal } from '@angular/core'; import { MatRippleModule } from '@angular/material/core'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatTooltipModule } from '@angular/material/tooltip'; import { PlaceRepositoryType } from '@placeos/ts-client'; import { IconComponent } from '../ui/icon.component'; +import { ItemTimestampsComponent } from '../ui/item-timestamps.component'; import { LoadingLabelDirective } from '../ui/loading-label.directive'; -import { DateFromPipe } from '../ui/pipes/date-from.pipe'; import { MarkdownPipe } from '../ui/pipes/markdown.pipe'; import { TranslatePipe } from '../ui/translate.pipe'; import { RepositoriesStateService } from './repositories-state.service'; @@ -15,185 +15,167 @@ import { maskUriCredentials } from './repositories.utilities'; @Component({ selector: 'repository-about', template: ` -
-
-
-
- {{ 'REPOS.FIELD_TYPE' | translate }} -
-
- {{ - (is_interface() - ? 'REPOS.INTERFACE_REPO' - : 'REPOS.DRIVER_REPO' - ) | translate - }} -
-
- {{ 'REPOS.FOLDER_NAME' | translate }} -
- -
- {{ 'REPOS.ROOT_PATH' | translate }} -
+
+
+
- {{ item().root_path }} - @if (item().root_path === '') { - Not set - } -
-
- {{ 'COMMON.CREATED_AT' | translate }} -
-
- - {{ item().created_at * 1000 | dateFrom }} - -
-
- {{ 'COMMON.UPDATED_AT' | translate }} -
-
- + {{ 'REPOS.FIELD_TYPE' | translate }} +
+
+ {{ + (is_interface() + ? 'REPOS.INTERFACE_REPO' + : 'REPOS.DRIVER_REPO' + ) | translate + }} +
+
+ {{ 'REPOS.FOLDER_NAME' | translate }} +
+
- {{ item().updated_at * 1000 | dateFrom }} - -
-
-
-
-
-
- {{ 'REPOS.URI' | translate }} -
- -
- {{ 'COMMON.GIT_BRANCH' | translate }} -
-
- - {{ item().branch }} - -
-
- {{ 'COMMON.GIT_COMMIT' | translate }} -
-
- + {{ item().folder_name }} + @if (!item().folder_name) { + + {{ + 'REPOS.FOLDER_NAME_EMPTY' + | translate + }} + + } + +
+
+ {{ 'REPOS.ROOT_PATH' | translate }} +
+
- {{ item().commit_hash || 'HEAD' }} - @if (commit() && commit() !== item().commit_hash) { - - ({{ commit() }}) - + {{ item().root_path }} + @if (item().root_path === '') { + Not set } - +
+
- @if (commit_error()) { -
- error -
-
- {{ 'REPOS.COMMIT_LOAD_ERROR' | translate }} -
-
- {{ commit_error() }} +
+
+
+
+ {{ 'REPOS.URI' | translate }} +
+ +
+ {{ 'COMMON.GIT_BRANCH' | translate }} +
+
+ + {{ item().branch }} + +
+
+ {{ 'COMMON.GIT_COMMIT' | translate }} +
+
+ + {{ item().commit_hash || 'HEAD' }} + @if ( + commit() && commit() !== item().commit_hash + ) { + + ({{ commit() }}) + + } + +
+ @if (commit_error()) { +
+ error +
+
+ {{ + 'REPOS.COMMIT_LOAD_ERROR' + | translate + }} +
+
+ {{ commit_error() }} +
-
- } - @if (is_interface()) { - - } + } + @if (is_interface()) { + + } +
+
+
+ @if (item()?.description) { +
+
+

+ {{ 'COMMON.FIELD_DESCRIPTION' | translate }} +

+
-
- - @if (item()?.description) { -
-
-

- {{ 'COMMON.FIELD_DESCRIPTION' | translate }} -

-
-
- } + } +
`, styles: [ ` @@ -213,15 +195,14 @@ import { maskUriCredentials } from './repositories.utilities'; `, ], imports: [ + ItemTimestampsComponent, LoadingLabelDirective, MarkdownPipe, TranslatePipe, MatProgressSpinnerModule, MatRippleModule, MatTooltipModule, - DateFromPipe, AsyncPipe, - DatePipe, IconComponent, ], }) diff --git a/src/app/repositories/repository-drivers.component.ts b/src/app/repositories/repository-drivers.component.ts index 92c3f7280..5e38ecefe 100644 --- a/src/app/repositories/repository-drivers.component.ts +++ b/src/app/repositories/repository-drivers.component.ts @@ -13,73 +13,75 @@ import { RepositoriesStateService } from './repositories-state.service'; @Component({ selector: 'repository-drivers', template: ` -

- {{ 'REPOS.AVAILABLE_DRIVERS' | translate }} -

- - @if (driver_list_error()) { -
-
- error -
-
- {{ 'REPOS.DRIVER_LIST_LOAD_ERROR' | translate }} -
-
- {{ driver_list_error() }} +
+

+ {{ 'REPOS.AVAILABLE_DRIVERS' | translate }} +

+ + @if (driver_list_error()) { +
+
+ error +
+
+ {{ 'REPOS.DRIVER_LIST_LOAD_ERROR' | translate }} +
+
+ {{ driver_list_error() }} +
-
- } - -
- -
-
- -
- -
-
+ } + +
+ +
+
+ +
+ +
+
+
`, styles: [ ` diff --git a/src/app/repositories/repository-form.component.ts b/src/app/repositories/repository-form.component.ts index 5ec1dc340..6f45190ba 100644 --- a/src/app/repositories/repository-form.component.ts +++ b/src/app/repositories/repository-form.component.ts @@ -13,7 +13,7 @@ import { viewChild, } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { FormField, disabled, form, submit } from '@angular/forms/signals'; +import { FormField, disabled, form } from '@angular/forms/signals'; import { DatePipe, SlicePipe } from '@angular/common'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; @@ -27,7 +27,6 @@ import { PlaceRepository, PlaceRepositoryType, addRepository, - cleanObject, listRemoteRepositoryBranches, listRemoteRepositoryCommits, listRemoteRepositoryDefaultBranch, @@ -37,17 +36,14 @@ import { updateRepository, } from '@placeos/ts-client'; import { AsyncHandler } from '../common/async-handler.class'; -import { readError } from '../common/errors'; -import { getInvalidSignalFields } from '../common/forms'; -import { HotkeysService } from '../common/hotkeys.service'; +import { mergeFormItem, onSaveHotkey, submitFormDialog } from '../common/forms'; import { i18n } from '../common/locale.service'; -import { notifyError, notifySuccess } from '../common/notifications'; +import { notifyError } from '../common/notifications'; import { DialogEvent, Identity } from '../common/types'; import { isValidUrl } from '../common/validation'; import { FullscreenModalShellComponent } from '../ui/fullscreen-modal-shell.component'; import { IconComponent } from '../ui/icon.component'; import { LoadingLabelDirective } from '../ui/loading-label.directive'; -import { DateFromPipe } from '../ui/pipes/date-from.pipe'; import { SettingsToggleComponent } from '../ui/settings-toggle.component'; import { TranslatePipe } from '../ui/translate.pipe'; import { @@ -514,7 +510,6 @@ export class RepositoryFormComponent extends AsyncHandler implements OnInit { private _data = inject<{ item: PlaceRepository; readonly?: string }>( MAT_DIALOG_DATA, ); - private _hotkey = inject(HotkeysService); private _injector = inject(Injector); private readonly _name = 'REPOS'; @@ -578,7 +573,6 @@ export class RepositoryFormComponent extends AsyncHandler implements OnInit { /** List of available types of repositories */ public readonly repo_types: WritableSignal = signal([]); public readonly show_password = signal(false); - public date_pipe = new DateFromPipe(); public readonly is_editing = signal(false); /** Emits when URI, username, or password fields lose focus */ public readonly credentials_blur = signal(0); @@ -610,6 +604,11 @@ export class RepositoryFormComponent extends AsyncHandler implements OnInit { return !this.is_interface() && this.formModel().id; } + constructor() { + super(); + onSaveHotkey(() => this.submit()); + } + public ngOnInit(): void { const item = this._data.item; const edit = !!item.id; @@ -624,63 +623,43 @@ export class RepositoryFormComponent extends AsyncHandler implements OnInit { this.follow_latest.set(this.formModel().commit_hash === 'HEAD'); this.is_editing.set(!!this.formModel().id); this._setupBranchAndCommitStreams(); - this.subscription( - 'save_item_key', - this._hotkey.listen(['KeyS'], () => this.submit()), - ); } public async submit(): Promise { - await submit(this.form, async () => { - // A disabled branch field skips validation, e.g. when branches fail to load - if (!this.formModel().branch) { - notifyError(i18n(`${this._name}.BRANCH_REQUIRED`)); - return; - } - const item = this._data.item; - this.saving.set(i18n(`${this._name}.SAVING`)); - const item_json = item.toJSON ? item.toJSON() : item; - const form_value = { ...this.formModel() }; - if (form_value.root_path) { - form_value.root_path = form_value.root_path.replace( - /^\/+|\/+$/g, - '', - ); - } - if (item.id) { - delete form_value.folder_name; - } - const form_item = ( - item.id - ? cleanObject({ ...item_json, ...form_value }, [undefined]) - : { ...item_json, ...form_value } - ) as PlaceRepository; - try { - const _item = await (form_item.id - ? updateRepository( - form_item.id, - form_item as unknown as PlaceRepository, - ) - : addRepository(form_item as unknown as PlaceRepository)); - this.event.emit({ reason: 'done', metadata: { item: _item } }); - notifySuccess(i18n(`${this._name}.SAVE_SUCCESS`)); - this._dialog_ref.close(); - } catch (err) { - this.saving.set(null); - notifyError( - i18n(`${this._name}.SAVE_ERROR`, { - error: await readError(err), - }), - ); - } - }); - if (this.form().invalid()) { - return notifyError( - i18n('COMMON.INVALID_FIELDS', { - field_list: getInvalidSignalFields(this.form).join(', '), - }), - ); - } + await submitFormDialog( + this.form, + { + loading: this.saving, + event: this.event, + close: () => this._dialog_ref.close(), + }, + `${this._name}.`, + () => { + // A disabled branch field skips validation, e.g. when branches fail to load + if (!this.formModel().branch) { + notifyError(i18n(`${this._name}.BRANCH_REQUIRED`)); + return undefined; + } + const item = this._data.item; + const form_value = { ...this.formModel() }; + if (form_value.root_path) { + form_value.root_path = form_value.root_path.replace( + /^\/+|\/+$/g, + '', + ); + } + if (item.id) { + delete form_value.folder_name; + } + const form_item = mergeFormItem( + item, + form_value, + ) as unknown as PlaceRepository; + return form_item.id + ? updateRepository(form_item.id, form_item) + : addRepository(form_item); + }, + ); } public setFollow(value: boolean) { diff --git a/src/app/systems/system-about.component.ts b/src/app/systems/system-about.component.ts index 7a8bf947e..c7a7a0168 100644 --- a/src/app/systems/system-about.component.ts +++ b/src/app/systems/system-about.component.ts @@ -1,12 +1,12 @@ -import { AsyncPipe, DatePipe } from '@angular/common'; +import { AsyncPipe } from '@angular/common'; import { Component, inject } from '@angular/core'; import { MatRippleModule } from '@angular/material/core'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatTooltipModule } from '@angular/material/tooltip'; import { SettingsFormComponent } from '../ui/forms/settings-form.component'; import { IconComponent } from '../ui/icon.component'; +import { ItemTimestampsComponent } from '../ui/item-timestamps.component'; import { LoadingLabelDirective } from '../ui/loading-label.directive'; -import { DateFromPipe } from '../ui/pipes/date-from.pipe'; import { MarkdownPipe } from '../ui/pipes/markdown.pipe'; import { TranslatePipe } from '../ui/translate.pipe'; import { SystemStateService } from './system-state.service'; @@ -128,40 +128,10 @@ import { SystemStateService } from './system-state.service';
{{ item()?.timezone }}
} -
- {{ 'COMMON.CREATED_AT' | translate }} -
-
- - {{ item().created_at * 1000 | dateFrom }} - -
-
- {{ 'COMMON.UPDATED_AT' | translate }} -
-
- - {{ item().updated_at * 1000 | dateFrom }} - -
+

@@ -232,9 +202,9 @@ import { SystemStateService } from './system-state.service'; `, ], imports: [ + ItemTimestampsComponent, LoadingLabelDirective, TranslatePipe, - DateFromPipe, MarkdownPipe, MatProgressSpinnerModule, SettingsFormComponent, @@ -242,7 +212,6 @@ import { SystemStateService } from './system-state.service'; MatTooltipModule, IconComponent, AsyncPipe, - DatePipe, ], }) export class SystemAboutComponent { diff --git a/src/app/systems/system-form.component.ts b/src/app/systems/system-form.component.ts index e194adf6e..4df0fe4ac 100644 --- a/src/app/systems/system-form.component.ts +++ b/src/app/systems/system-form.component.ts @@ -5,11 +5,10 @@ import { effect, EventEmitter, inject, - OnInit, Output, signal, } from '@angular/core'; -import { form, FormField, submit } from '@angular/forms/signals'; +import { form, FormField } from '@angular/forms/signals'; import { MatChipEditedEvent, MatChipInputEvent, @@ -18,7 +17,6 @@ import { import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { addSystem, - cleanObject, PlaceResource, PlaceSystem, updateSystem, @@ -28,20 +26,18 @@ import { querySupportZones as queryZones } from '../common/support-access'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; -import { AsyncHandler } from '../common/async-handler.class'; import { addSignalChipItem, editSignalChipItem, - getInvalidSignalFields, + mergeFormItem, + onSaveHotkey, removeSignalChipItem, + submitFormDialog, } from '../common/forms'; -import { HotkeysService } from '../common/hotkeys.service'; import { i18n } from '../common/locale.service'; -import { notifyError, notifySuccess } from '../common/notifications'; import { TIMEZONES_IANA } from '../common/timezones'; -import { DialogEvent, Identity } from '../common/types'; +import { DialogEvent } from '../common/types'; -import { readError } from '../common/errors'; import { CounterComponent } from '../ui/counter.component'; import { ImageListFieldComponent } from '../ui/custom-fields/image-list-field.component'; import { ItemSearchFieldComponent } from '../ui/custom-fields/item-search-field.component'; @@ -660,14 +656,13 @@ const TIMEZONE_OPTION_LIMIT = 50; FullscreenModalShellComponent, ], }) -export class SystemFormComponent extends AsyncHandler implements OnInit { +export class SystemFormComponent { private _dialog_ref = inject>(MatDialogRef); private _data = inject<{ item: PlaceSystem; readonly?: string }>( MAT_DIALOG_DATA, ); private readonly _name = 'SYSTEMS'; - private _hotkey = inject(HotkeysService); public readonly item = signal(this._data.item); @Output() public event = new EventEmitter(); @@ -704,7 +699,7 @@ export class SystemFormComponent extends AsyncHandler implements OnInit { }); constructor() { - super(); + onSaveHotkey(() => this.submit()); effect(() => { const model = this.formModel(); const zone = model.zone; @@ -720,61 +715,36 @@ export class SystemFormComponent extends AsyncHandler implements OnInit { }); } - public ngOnInit(): void { - this.subscription( - 'save_item_key', - this._hotkey.listen(['KeyS'], () => this.submit()), - ); - } - public async submit(): Promise { if (this.loading()) return; - await submit(this.form, async () => { - const item = this._data.item as unknown as PlaceResource; - this.loading.set(i18n(`${this._name}.SAVING`)); - const item_json = item.toJSON ? item.toJSON() : item; - // `zone` is UI-only; the backend uses `zones`, which the effect - // keeps in sync. Strip it so we never POST `zone: null`. - const { zone: _zone, ...form_value } = this.formModel(); - const form_item = ( - item.id - ? cleanObject({ ...item_json, ...form_value }, [undefined]) - : { ...item_json, ...form_value } - ) as Identity; - const processed_item = { - ...form_item, - support_url: this.processURL( - form_item as unknown as PlaceSystem, - (form_item as Identity & { support_url?: string }) - .support_url || '', - ), - }; - try { - const _item = await (processed_item.id - ? updateSystem( - processed_item.id as string, - processed_item as unknown as PlaceSystem, - ) - : addSystem(processed_item as unknown as PlaceSystem)); - this.event.emit({ reason: 'done', metadata: { item: _item } }); - notifySuccess(i18n(`${this._name}.SAVE_SUCCESS`)); - this._dialog_ref.close(); - } catch (err) { - this.loading.set(null); - notifyError( - i18n(`${this._name}.SAVE_ERROR`, { - error: await readError(err), - }), - ); - } - }); - if (this.form().invalid()) { - return notifyError( - i18n('COMMON.INVALID_FIELDS', { - field_list: getInvalidSignalFields(this.form).join(', '), - }), - ); - } + await submitFormDialog( + this.form, + { + loading: this.loading, + event: this.event, + close: () => this._dialog_ref.close(), + }, + `${this._name}.`, + () => { + // `zone` is UI-only; the backend uses `zones`, which the effect + // keeps in sync. Strip it so we never POST `zone: null`. + const { zone: _zone, ...form_value } = this.formModel(); + const form_item = mergeFormItem( + this._data.item as unknown as PlaceResource, + form_value, + ) as unknown as PlaceSystem; + const processed_item = { + ...form_item, + support_url: this.processURL( + form_item, + form_item.support_url || '', + ), + } as unknown as PlaceSystem; + return processed_item.id + ? updateSystem(processed_item.id, processed_item) + : addSystem(processed_item); + }, + ); } /** diff --git a/src/app/systems/system-modules.component.ts b/src/app/systems/system-modules.component.ts index c093a14a1..88bbd2591 100644 --- a/src/app/systems/system-modules.component.ts +++ b/src/app/systems/system-modules.component.ts @@ -23,11 +23,10 @@ import { queryModules, showModule, } from '@placeos/ts-client'; -import { AsyncHandler } from '../common/async-handler.class'; import { describeError } from '../common/errors'; import { i18n } from '../common/locale.service'; import { notifyError, notifySuccess } from '../common/notifications'; -import { AppLink, HashMap } from '../common/types'; +import { AppLink } from '../common/types'; import { ModuleStateModalData, ViewModuleStateModalComponent, @@ -407,12 +406,10 @@ import { SystemStateService } from './system-state.service'; MatProgressBarModule, ], }) -export class SystemModulesComponent extends AsyncHandler { +export class SystemModulesComponent { private _service = inject(SystemStateService); private _dialog = inject(MatDialog); - /** Whether a device should be listened to */ - public device_listener: HashMap = {}; /** Store for ID of new module to add to system */ public readonly new_module = signal(''); /** Whether to show exec block */ @@ -562,7 +559,6 @@ export class SystemModulesComponent extends AsyncHandler { } constructor() { - super(); // Tab is reused across systems. Drop the pending module on change. effect(() => { this._item_id(); diff --git a/src/app/systems/system-state.service.ts b/src/app/systems/system-state.service.ts index f3c81b3c9..acd63f07d 100644 --- a/src/app/systems/system-state.service.ts +++ b/src/app/systems/system-state.service.ts @@ -41,8 +41,8 @@ import { describeError, readError } from '../common/errors'; import { ActiveItemService } from '../common/item.service'; import { reportLoadError } from '../common/load-error'; import { notifyError, notifySuccess } from '../common/notifications'; -import { waitForEvent } from '../common/signals'; -import { DialogEvent, HashMap, Identity } from '../common/types'; +import { waitForDialog } from '../common/signals'; +import { HashMap, Identity } from '../common/types'; import { ConfirmModalData, openConfirmModal, @@ -403,13 +403,7 @@ export class SystemStateService extends AsyncHandler { ), }, }); - const details = await Promise.race([ - waitForEvent( - ref.componentInstance.event, - (_: DialogEvent) => _.reason === 'action', - ), - waitForEvent(ref.afterClosed()), - ]); + const details = await waitForDialog(ref, 'action'); if (details?.reason !== 'action') return ref.close(); const system = ref.componentInstance.item as PlaceSystem; if (!system) return ref.close(); @@ -444,20 +438,12 @@ export class SystemStateService extends AsyncHandler { queryTriggers({ q: _ }).then((resp) => resp.data), }, }); - const details = await Promise.race([ - waitForEvent( - ref.componentInstance.event, - (_: DialogEvent) => _.reason === 'action', - ), - waitForEvent(ref.afterClosed()), - ]); + const details = await waitForDialog(ref, 'action'); if (details?.reason !== 'action') return ref.close(); try { - const t = await this.addTrigger( + return await this.addTrigger( ref.componentInstance.item as PlaceTrigger, ); - this.triggersChanged(); - return t; } catch (err) { notifyError( `Error adding trigger to system. Error: ${describeError(err)}`, @@ -489,13 +475,7 @@ export class SystemStateService extends AsyncHandler { }, }); const instance = ref.componentInstance; - const details = await Promise.race([ - waitForEvent( - instance.event, - (_: DialogEvent) => _.reason === 'action', - ), - waitForEvent(ref.afterClosed()), - ]); + const details = await waitForDialog(ref, 'action'); if (details?.reason !== 'action') return; instance.loading.set('Saving trigger settings...'); diff --git a/src/app/systems/system-triggers.component.ts b/src/app/systems/system-triggers.component.ts index 847bf30c7..b3ac9c6ee 100644 --- a/src/app/systems/system-triggers.component.ts +++ b/src/app/systems/system-triggers.component.ts @@ -2,7 +2,6 @@ import { Component, computed, inject, model } from '@angular/core'; import { PlaceTrigger } from '@placeos/ts-client'; import { copyToClipboard, unique } from '../common/general'; import { notifyInfo } from '../common/notifications'; -import { toSignal } from '../common/signals'; import { FormsModule } from '@angular/forms'; import { MatRippleModule } from '@angular/material/core'; @@ -234,9 +233,7 @@ export class SystemTriggersComponent { public readonly loading = this._service.loading; - public readonly service_triggers = toSignal(this._service.triggers, { - initialValue: [] as (PlaceTrigger & { name: string })[], - }); + public readonly service_triggers = this._service.triggers; public readonly trigger_state: HashMap = {}; diff --git a/src/app/systems/systems.component.ts b/src/app/systems/systems.component.ts index d1a002557..6a17daf4e 100644 --- a/src/app/systems/systems.component.ts +++ b/src/app/systems/systems.component.ts @@ -1,147 +1,34 @@ -import { Component, computed, inject, signal } from '@angular/core'; -import { MatRippleModule } from '@angular/material/core'; -import { MatTabNavPanel } from '@angular/material/tabs'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { RouterModule } from '@angular/router'; +import { Component, computed, inject } from '@angular/core'; import { PlaceSystem } from '@placeos/ts-client'; import { extensionsForItem } from '../common/api'; -import { PlaceDebugService } from '../common/debug.service'; import { ActiveItemService } from '../common/item.service'; import { i18n } from '../common/locale.service'; import { isSubsystemUser } from '../common/support-access'; -import { DebugOutputComponent } from '../ui/debug-output.component'; -import { IconComponent } from '../ui/icon.component'; -import { ItemDetailsSkeletonComponent } from '../ui/item-details-skeleton.component'; -import { ItemDetailsComponent } from '../ui/item-details.component'; -import { ItemSelectionComponent } from '../ui/item-selection.component'; -import { ItemSidebarComponent } from '../ui/item-sidebar.component'; -import { ItemTab, ItemTablistComponent } from '../ui/item-tablist.component'; -import { SidebarMenuComponent } from '../ui/sidebar-menu.component'; -import { TranslatePipe } from '../ui/translate.pipe'; +import { ItemShellComponent } from '../ui/item-shell.component'; +import { ItemTab } from '../ui/item-tablist.component'; import { SystemStateService } from './system-state.service'; @Component({ selector: 'new-systems-view', template: ` -
- -
-
-
- @if (debug_position() === 'below') { - - } -
- @if (debug_position() === 'side') { - - } -
+ `, - styles: [``], - imports: [ - MatTabNavPanel, - DebugOutputComponent, - IconComponent, - MatRippleModule, - MatTooltipModule, - TranslatePipe, - ItemTablistComponent, - ItemDetailsComponent, - ItemDetailsSkeletonComponent, - ItemSelectionComponent, - ItemSidebarComponent, - SidebarMenuComponent, - RouterModule, - ], + imports: [ItemShellComponent], }) export class SystemsComponent { protected _service = inject(SystemStateService); private _item = inject(ActiveItemService); - private _debug = inject(PlaceDebugService); public readonly item = computed( () => this._item.active_item$() as PlaceSystem | null, ); - public readonly loading = this._item.loading; - public readonly open_menu = signal(false); public readonly name = 'systems'; - public readonly scroll = signal(0); public readonly counts = this._service.counts; public readonly extensions = computed(() => extensionsForItem(this.item(), this.name), @@ -190,8 +77,4 @@ export class SystemsComponent { (tab) => !isSubsystemUser() || !['history'].includes(tab.id), ), ); - public readonly debug_position = this._debug.position; - public readonly canCreate = () => this._item.canMutate(2); - public readonly newItem = () => this._item.create(); - public readonly bulkAdd = () => this._item.bulkAdd(); } diff --git a/src/app/systems/systems.routes.ts b/src/app/systems/systems.routes.ts index 137a20655..f5816d5ec 100644 --- a/src/app/systems/systems.routes.ts +++ b/src/app/systems/systems.routes.ts @@ -1,5 +1,5 @@ import { Routes } from '@angular/router'; -import { AuthorisedAdminGuard } from '../ui/guards/authorised-admin.guard'; +import { authorisedAdminGuard } from '../ui/guards/authorised.guard'; export const ROUTES: Routes = [ { @@ -38,8 +38,8 @@ export const ROUTES: Routes = [ { path: 'metadata', loadComponent: () => - import('./system-metadata.component').then( - (m) => m.SystemMetadataComponent, + import('../ui/item-metadata.component').then( + (m) => m.ItemMetadataComponent, ), }, { @@ -51,7 +51,7 @@ export const ROUTES: Routes = [ }, { path: 'history', - canActivate: [AuthorisedAdminGuard], + canActivate: [authorisedAdminGuard], loadComponent: () => import('../ui/settings-history-view.component').then( (m) => m.SettingsHistoryViewComponent, diff --git a/src/app/systems/systems.utilities.ts b/src/app/systems/systems.utilities.ts index 7e5726e45..b8c497b14 100644 --- a/src/app/systems/systems.utilities.ts +++ b/src/app/systems/systems.utilities.ts @@ -1,15 +1,15 @@ import { AbstractControl } from '@angular/forms'; import { PlaceSystem, PlaceZone } from '@placeos/ts-client'; -import * as yaml from 'js-yaml'; import { email, required, SchemaFn, validate } from '@angular/forms/signals'; +import * as yaml from 'js-yaml'; import { isValidUrl } from '../common/validation'; export function validateYAML(control: AbstractControl) { const value = control.value || ''; let message = ''; try { - yaml.load(value, { strict: true }); + yaml.load(value); } catch (e) { // console.error(e); message = e.message; diff --git a/src/app/triggers/trigger-about.component.ts b/src/app/triggers/trigger-about.component.ts index b54fec09e..7a72cbae8 100644 --- a/src/app/triggers/trigger-about.component.ts +++ b/src/app/triggers/trigger-about.component.ts @@ -8,17 +8,14 @@ import { TriggerTimeCondition, querySystems, } from '@placeos/ts-client'; -import { toSignal } from '../common/signals'; -import { AsyncPipe, DatePipe, JsonPipe } from '@angular/common'; +import { AsyncPipe, JsonPipe } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { MatRippleModule } from '@angular/material/core'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { AsyncHandler } from '../common/async-handler.class'; import { ItemSearchFieldComponent } from '../ui/custom-fields/item-search-field.component'; import { IconComponent } from '../ui/icon.component'; -import { DateFromPipe } from '../ui/pipes/date-from.pipe'; -import { FormatListPipe } from '../ui/pipes/format-list.pipe'; +import { ItemTimestampsComponent } from '../ui/item-timestamps.component'; import { MarkdownPipe } from '../ui/pipes/markdown.pipe'; import { SimpleTableComponent } from '../ui/simple-table.component'; import { TranslatePipe } from '../ui/translate.pipe'; @@ -27,283 +24,269 @@ import { TriggerStateService } from './trigger-state.service'; @Component({ selector: 'trigger-about', template: ` -
-
-
- {{ 'COMMON.CREATED_AT' | translate }} -
-
- - {{ item.created_at * 1000 | dateFrom }} - -
-
- {{ 'COMMON.UPDATED_AT' | translate }} -
-
- - {{ item.updated_at * 1000 | dateFrom }} - -
-
-
- @if (item?.description) { -
-
-

- {{ 'COMMON.FIELD_DESCRIPTION' | translate }} -

+
+
-
- } -
-
- - -
-
-
- {{ 'TRIGGERS.CONDITIONS' | translate }} -
- -
-
- - - -
- {{ row.type === 'at' ? 'At time' : 'CRON' }} - {{ row.type === 'at' ? row.time : row.cron }} +
-
- -
-
{{ row.left | json }}
- - {{ row.operator }} - -
{{ row.right | json }}
-
-
- -
- - + {{ 'COMMON.FIELD_DESCRIPTION' | translate }} +

+
- - -
-
- {{ 'TRIGGERS.ACTIONS' | translate }} + } +
+
+ +
- -
-
- - - -
-
- {{ row.mod }} -
-
{{ row.method }}({{ row.args | json }})
-
-
- -
- {{ row.emails.length }} Address(es)  | Body Length: {{ row.content.length }} +
+
+ {{ 'TRIGGERS.CONDITIONS' | translate }}
- - -
- - +
+
+ + + +
- delete - + {{ row.type === 'at' ? 'At time' : 'CRON' }} + {{ row.type === 'at' ? row.time : row.cron }} +
+
+ +
+
{{ row.left | json }}
+ + {{ row.operator }} + +
{{ row.right | json }}
+
+
+ +
+ + +
+
+
+
+
+ {{ 'TRIGGERS.ACTIONS' | translate }}
- -
+ + +
+ + + +
+
+ {{ row.mod }} +
+
{{ row.method }}({{ row.args | json }})
+
+
+ +
+ {{ row.emails.length }} Address(es)  | Body Length: {{ row.content.length }} +
+
+ +
+ + +
+
+
+
`, styles: [ ` @@ -314,31 +297,27 @@ import { TriggerStateService } from './trigger-state.service'; `, ], imports: [ + ItemTimestampsComponent, TranslatePipe, IconComponent, MatRippleModule, MatTooltipModule, - FormatListPipe, SimpleTableComponent, ItemSearchFieldComponent, FormsModule, - DateFromPipe, MarkdownPipe, AsyncPipe, - DatePipe, JsonPipe, ], }) -export class TriggerAboutComponent extends AsyncHandler { +export class TriggerAboutComponent { private _service = inject(TriggerStateService); /** System to use for conditions with systen variables and functions */ public readonly template_system = signal( undefined, ); - private readonly _item = toSignal(this._service.item, { - initialValue: {} as PlaceTrigger, - }); + private readonly _item = this._service.item; public get item(): PlaceTrigger { return this._item(); } diff --git a/src/app/triggers/trigger-form.component.ts b/src/app/triggers/trigger-form.component.ts index 56c5232e3..8556b6021 100644 --- a/src/app/triggers/trigger-form.component.ts +++ b/src/app/triggers/trigger-form.component.ts @@ -6,23 +6,14 @@ import { inject, signal, } from '@angular/core'; -import { FormField, form, submit } from '@angular/forms/signals'; +import { FormField, form } from '@angular/forms/signals'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; -import { - PlaceTrigger, - addTrigger, - cleanObject, - updateTrigger, -} from '@placeos/ts-client'; -import { AsyncHandler } from '../common/async-handler.class'; -import { readError } from '../common/errors'; -import { getInvalidSignalFields } from '../common/forms'; -import { HotkeysService } from '../common/hotkeys.service'; +import { PlaceTrigger, addTrigger, updateTrigger } from '@placeos/ts-client'; +import { mergeFormItem, onSaveHotkey, submitFormDialog } from '../common/forms'; import { i18n } from '../common/locale.service'; -import { notifyError, notifySuccess } from '../common/notifications'; import { DialogEvent } from '../common/types'; import { CounterComponent } from '../ui/counter.component'; import { FullscreenModalShellComponent } from '../ui/fullscreen-modal-shell.component'; @@ -174,13 +165,12 @@ import { FullscreenModalShellComponent, ], }) -export class TriggerFormComponent extends AsyncHandler implements OnInit { +export class TriggerFormComponent implements OnInit { private _dialog_ref = inject>(MatDialogRef); private _data = inject<{ item: PlaceTrigger; readonly?: string }>( MAT_DIALOG_DATA, ); - private _hotkey = inject(HotkeysService); private readonly _name = 'TRIGGERS'; @Output() public event = new EventEmitter(); @@ -194,49 +184,34 @@ export class TriggerFormComponent extends AsyncHandler implements OnInit { public readonly render_debounce = (v: number) => `${v} ms`; + constructor() { + onSaveHotkey(() => this.submit()); + } + public ngOnInit(): void { const item = this._data.item; const edit = !!item.id; this.heading.set(i18n(`${this._name}.${edit ? 'EDIT' : 'NEW'}`)); - this.subscription( - 'save_item_key', - this._hotkey.listen(['KeyS'], () => this.submit()), - ); } public async submit(): Promise { - await submit(this.form, async () => undefined); - if (this.form().invalid()) { - return notifyError( - i18n('COMMON.INVALID_FIELDS', { - field_list: getInvalidSignalFields(this.form).join(', '), - }), - ); - } - const item = this._data.item; - this.loading.set(i18n(`${this._name}.SAVING`)); - const item_json = item.toJSON ? item.toJSON() : item; - const form_item = ( - item.id - ? cleanObject({ ...item_json, ...this.formModel() }, [ - undefined, - ]) - : { ...item_json, ...this.formModel() } - ) as PlaceTrigger; - try { - const _item = await (form_item.id - ? updateTrigger(form_item.id, form_item) - : addTrigger(form_item)); - this.event.emit({ reason: 'done', metadata: { item: _item } }); - notifySuccess(i18n(`${this._name}.SAVE_SUCCESS`)); - this._dialog_ref.close(); - } catch (err) { - this.loading.set(null); - notifyError( - i18n(`${this._name}.SAVE_ERROR`, { - error: await readError(err), - }), - ); - } + await submitFormDialog( + this.form, + { + loading: this.loading, + event: this.event, + close: () => this._dialog_ref.close(), + }, + `${this._name}.`, + () => { + const form_item = mergeFormItem( + this._data.item, + this.formModel(), + ) as unknown as PlaceTrigger; + return form_item.id + ? updateTrigger(form_item.id, form_item) + : addTrigger(form_item); + }, + ); } } diff --git a/src/app/triggers/trigger-instances.component.ts b/src/app/triggers/trigger-instances.component.ts index c3d39b044..380defc9e 100644 --- a/src/app/triggers/trigger-instances.component.ts +++ b/src/app/triggers/trigger-instances.component.ts @@ -1,6 +1,4 @@ import { Component, inject } from '@angular/core'; -import { PlaceTrigger } from '@placeos/ts-client'; -import { toSignal } from '../common/signals'; import { HashMap } from '../common/types'; @@ -8,7 +6,6 @@ import { MatRippleModule } from '@angular/material/core'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; -import { AsyncHandler } from '../common/async-handler.class'; import { IconComponent } from '../ui/icon.component'; import { LoadingLabelDirective } from '../ui/loading-label.directive'; import { DateFromPipe } from '../ui/pipes/date-from.pipe'; @@ -19,78 +16,80 @@ import { TriggerStateService } from './trigger-state.service'; @Component({ selector: 'trigger-systems', template: ` - - - -
-
- - - {{ row.name || row.zone_id || row.control_system_id }} - - - -
- {{ +row.created_at * 1000 | dateFrom }} -
-
- -
- -
-
+ {{ row.name || row.zone_id || row.control_system_id }} + + + +
+ {{ +row.created_at * 1000 | dateFrom }} +
+
+ +
+ +
+
+
`, styles: [ ` @@ -112,21 +111,15 @@ import { TriggerStateService } from './trigger-state.service'; DateFromPipe, ], }) -export class TriggerInstancesComponent extends AsyncHandler { +export class TriggerInstancesComponent { private _service = inject(TriggerStateService); /** List of systems associated with the trigger */ public readonly instances = this._service.instances; - public readonly loading = toSignal(this._service.loading, { - initialValue: false, - }); + public readonly loading = this._service.loading; /** Map of systems ids to connected status */ public connected: HashMap = {}; public readonly deleteTrigger = (s) => this._service.removeTriggerFromParent(s); - - public get item(): PlaceTrigger { - return this._service.active_item as PlaceTrigger; - } } diff --git a/src/app/triggers/trigger-state.service.ts b/src/app/triggers/trigger-state.service.ts index 2181e1a73..52fcadd7b 100644 --- a/src/app/triggers/trigger-state.service.ts +++ b/src/app/triggers/trigger-state.service.ts @@ -21,7 +21,7 @@ import { ActiveItemService } from '../common/item.service'; import { reportLoadError } from '../common/load-error'; import { i18n } from '../common/locale.service'; import { notifyError, notifySuccess } from '../common/notifications'; -import { waitForEvent } from '../common/signals'; +import { waitForDialog } from '../common/signals'; import { DialogEvent, Identity } from '../common/types'; import { openConfirmModal } from '../overlays/confirm-modal.component'; import { @@ -85,13 +85,9 @@ export class TriggerStateService { }, }); const result: DialogEvent<{ trigger: PlaceTrigger }> | null = - (await Promise.race([ - waitForEvent( - ref.componentInstance.event, - (event: DialogEvent) => event.reason === 'done', - ), - waitForEvent(ref.afterClosed()), - ])) as DialogEvent<{ trigger: PlaceTrigger }> | null; + (await waitForDialog(ref, 'done')) as DialogEvent<{ + trigger: PlaceTrigger; + }> | null; if (!result?.reason || !result.metadata?.trigger) return; this._service.replaceItem( result.metadata.trigger as unknown as Identity, @@ -118,13 +114,9 @@ export class TriggerStateService { }, }); const result: DialogEvent<{ trigger: PlaceTrigger }> | null = - (await Promise.race([ - waitForEvent( - ref.componentInstance.event, - (event: DialogEvent) => event.reason === 'done', - ), - waitForEvent(ref.afterClosed()), - ])) as DialogEvent<{ trigger: PlaceTrigger }> | null; + (await waitForDialog(ref, 'done')) as DialogEvent<{ + trigger: PlaceTrigger; + }> | null; if (!result?.reason || !result.metadata?.trigger) return; this._service.replaceItem( result.metadata.trigger as unknown as Identity, diff --git a/src/app/triggers/triggers.component.ts b/src/app/triggers/triggers.component.ts index 0ba11f002..12e33633c 100644 --- a/src/app/triggers/triggers.component.ts +++ b/src/app/triggers/triggers.component.ts @@ -1,147 +1,32 @@ -import { Component, computed, inject, signal } from '@angular/core'; -import { MatRippleModule } from '@angular/material/core'; -import { MatTabNavPanel } from '@angular/material/tabs'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { RouterModule } from '@angular/router'; -import { PlaceTrigger } from '@placeos/ts-client'; +import { Component, computed, inject } from '@angular/core'; import { extensionsForItem } from '../common/api'; -import { AsyncHandler } from '../common/async-handler.class'; -import { PlaceDebugService } from '../common/debug.service'; import { ActiveItemService } from '../common/item.service'; import { i18n } from '../common/locale.service'; -import { toSignal } from '../common/signals'; -import { DebugOutputComponent } from '../ui/debug-output.component'; -import { IconComponent } from '../ui/icon.component'; -import { ItemDetailsSkeletonComponent } from '../ui/item-details-skeleton.component'; -import { ItemDetailsComponent } from '../ui/item-details.component'; -import { ItemSelectionComponent } from '../ui/item-selection.component'; -import { ItemSidebarComponent } from '../ui/item-sidebar.component'; -import { ItemTablistComponent } from '../ui/item-tablist.component'; -import { SidebarMenuComponent } from '../ui/sidebar-menu.component'; -import { TranslatePipe } from '../ui/translate.pipe'; +import { ItemShellComponent } from '../ui/item-shell.component'; import { TriggerStateService } from './trigger-state.service'; @Component({ selector: 'new-triggers-view', template: ` -
- -
-
-
- @if (debug_position() === 'below') { - - } -
- @if (debug_position() === 'side') { - - } -
+ `, - styles: [``], - imports: [ - MatTabNavPanel, - DebugOutputComponent, - IconComponent, - TranslatePipe, - RouterModule, - MatRippleModule, - MatTooltipModule, - ItemTablistComponent, - ItemDetailsComponent, - ItemDetailsSkeletonComponent, - ItemSelectionComponent, - ItemSidebarComponent, - SidebarMenuComponent, - ], + imports: [ItemShellComponent], }) -export class TriggersComponent extends AsyncHandler { +export class TriggersComponent { protected _service = inject(ActiveItemService); - private _debug = inject(PlaceDebugService); private _state = inject(TriggerStateService); public readonly name = 'triggers'; - public open_menu = false; public readonly instance_count = computed( () => this._state.instances().length, ); - public readonly loading = toSignal(this._service.loading, { - initialValue: false, - }); - public readonly debug_position = this._debug.position; - public readonly item = toSignal(this._service.item, { - initialValue: null as PlaceTrigger | null, - }); - public readonly newItem = () => this._service.create(); - public readonly scroll = signal(0); + public readonly item = this._service.item; public readonly tab_list = computed(() => [ { id: 'about', diff --git a/src/app/ui/application-picker-tooltip.component.ts b/src/app/ui/application-picker-tooltip.component.ts index 851458359..6f9eb265b 100644 --- a/src/app/ui/application-picker-tooltip.component.ts +++ b/src/app/ui/application-picker-tooltip.component.ts @@ -17,6 +17,17 @@ interface SidebarApplication { const PAGE_SIZE = 9; +/** Whether an application redirect URI can open in a browser tab */ +export function isSupportedRedirectUri(uri: string) { + if (!uri) return false; + try { + const protocol = new URL(uri, location.origin).protocol; + return protocol === 'http:' || protocol === 'https:'; + } catch { + return false; + } +} + @Component({ selector: 'application-picker-tooltip', template: ` @@ -141,7 +152,7 @@ export class ApplicationPickerTooltipComponent implements OnInit { authority_id: active_authority.id, } as Record).catch(() => null); const applications = (response?.data || []) - .filter((app) => this.isSupportedRedirectUri(app?.redirect_uri)) + .filter((app) => isSupportedRedirectUri(app?.redirect_uri)) .sort((lhs, rhs) => (lhs.name || this.nameFromUri(lhs.redirect_uri)).localeCompare( rhs.name || this.nameFromUri(rhs.redirect_uri), @@ -209,16 +220,6 @@ export class ApplicationPickerTooltipComponent implements OnInit { } } - private isSupportedRedirectUri(uri: string) { - if (!uri) return false; - try { - const protocol = new URL(uri, location.origin).protocol; - return protocol === 'http:' || protocol === 'https:'; - } catch { - return false; - } - } - private faviconUrls(uri: string) { const path = this.redirectPath(uri); return [ diff --git a/src/app/ui/custom-fields/date-calendar.component.ts b/src/app/ui/custom-fields/date-calendar.component.ts index 06694943c..6814a1e8d 100644 --- a/src/app/ui/custom-fields/date-calendar.component.ts +++ b/src/app/ui/custom-fields/date-calendar.component.ts @@ -21,7 +21,6 @@ import { startOfMonth, startOfWeek, } from 'date-fns'; -import { AsyncHandler } from '../../common/async-handler.class'; import { SettingsService } from '../../common/settings.service'; import { IconComponent } from '../icon.component'; @@ -45,10 +44,7 @@ interface DateItem { icon matRipple name="schedule-next-month" - [disabled]=" - disabled() || - date_list()[0]?.id < from() - " + [disabled]="disabled() || date_list()[0]?.id < from()" (click)="changeMonth(-1)" > chevron_left @@ -58,10 +54,7 @@ interface DateItem { icon matRipple name="schedule-previous-month" - [disabled]=" - disabled() || - date_list()[34]?.id > to() - " + [disabled]="disabled() || date_list()[34]?.id > to()" (click)="changeMonth(1)" > chevron_right @@ -124,7 +117,6 @@ interface DateItem { imports: [IconComponent, DatePipe, SlicePipe], }) export class DateCalendarComponent - extends AsyncHandler implements ControlValueAccessor, OnChanges, OnInit { private _settings = inject(SettingsService); diff --git a/src/app/ui/custom-fields/date-field.component.ts b/src/app/ui/custom-fields/date-field.component.ts index 85d7160eb..dee36bc20 100644 --- a/src/app/ui/custom-fields/date-field.component.ts +++ b/src/app/ui/custom-fields/date-field.component.ts @@ -2,9 +2,6 @@ import { DatePipe } from '@angular/common'; import { Component, forwardRef, - inject, - Injector, - input, model, OnInit, signal, @@ -14,22 +11,13 @@ import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, - NgControl, } from '@angular/forms'; import { addYears, endOfDay, set, startOfDay } from 'date-fns'; -import { AsyncHandler } from '../../common/async-handler.class'; -import { getTimezoneOffsetString } from '../../common/timezone-helpers'; import { CustomTooltipComponent } from '../custom-tooltip.component'; import { IconComponent } from '../icon.component'; import { TranslatePipe } from '../translate.pipe'; import { DateCalendarComponent } from './date-calendar.component'; -export enum TimezoneDiffRange { - Both, - Start, - End, -} - @Component({ selector: 'a-date-field', template: ` @@ -48,43 +36,25 @@ export enum TimezoneDiffRange { >
@if (date()) { - {{ date() | date: date_format }} + {{ date() | date: 'MMMM d, yyyy' }} } @else { {{ 'COMMON.DATE_EMPTY' | translate }} }
- @if (timezone() && tz) { -
- @if (range() !== 2) { - {{ start_of_day }} - } - @if (range() === 0) { - - - } - @if (range() !== 1) { - {{ end_of_day }} - } -
- }
today
-
- @if (has_error) { - - } -
+
@@ -107,91 +77,26 @@ export enum TimezoneDiffRange { DatePipe, ], }) -export class DateFieldComponent - extends AsyncHandler - implements OnInit, ControlValueAccessor -{ - private _injector = inject(Injector); - - /** Earliest date available the user is allowed to pick */ - public readonly from = input(startOfDay(Date.now()).valueOf()); - /** Latest date available the user is allowed to pick */ - public readonly to = input(undefined); - /** Index of the day to start the week on when displaying the calendar */ - public readonly week_start = input(0); - public readonly use_24hr = input(false); +export class DateFieldComponent implements OnInit, ControlValueAccessor { /** Whether form control is disabled */ public readonly disabled = model(false); - public readonly short = input(false); - public readonly timezone = input(''); - public readonly range = input(TimezoneDiffRange.Both); /** Currently selected date */ public readonly date = signal(null); public readonly now = Date.now(); + /** Earliest date the user is allowed to pick */ + public readonly from = startOfDay(this.now).valueOf(); + /** Latest date the user is allowed to pick */ + public readonly until = addYears(endOfDay(this.now), 1).valueOf(); /** Form control on change handler */ private _onChange: (_: number) => void; /** Form control on touch handler */ private _onTouch: (_: number) => void; - private _control?: NgControl; - - public get date_format() { - return this.short() ? 'MMM d, yyyy' : 'MMMM d, yyyy'; - } - - public get time_format() { - return this.use_24hr() ? 'HH : mm' : 'h : mm a'; - } - - private _date_pipe = new DatePipe('en'); - - public get start_of_day() { - const start = startOfDay(this.date()).valueOf(); - const format = `MMM d, ${this.time_format}${this.range() === 1 ? ' (z)' : ''}`; - return this._date_pipe.transform(start, format, this.tz); - } - - public get end_of_day() { - const end = endOfDay(this.date()).valueOf(); - const format = `MMM d, ${this.time_format}${this.range() === 1 ? ' (z)' : ''}`; - return this._date_pipe.transform(end, format, this.tz); - } - - public get has_error(): boolean { - return this._control?.invalid && this._control?.touched; - } - - private _local_tz = getTimezoneOffsetString( - Intl.DateTimeFormat().resolvedOptions().timeZone, - ); - - public get tz() { - const tz = this.timezone(); - if (!tz) return ''; - const tz_offset = getTimezoneOffsetString(tz); - return tz_offset === this._local_tz ? '' : tz_offset; - } private readonly _tooltip = viewChild(CustomTooltipComponent); - /** First allowed date on the calendar */ - public get fromDate(): number { - const date = new Date(this.from()); - return ( - isNaN(date.getTime()) ? startOfDay(new Date()) : date - ).valueOf(); - } - /** Current date value */ - public get until(): number { - const date = new Date(this.to()); - return ( - isNaN(date.getTime()) ? addYears(endOfDay(new Date()), 1) : date - ).valueOf(); - } - public ngOnInit() { - this._control = this._injector.get(NgControl); this.date.set(Date.now()); } @@ -208,9 +113,7 @@ export class DateFieldComponent minutes: old_date.getMinutes(), }).valueOf(); // Check that new date is before from - if (new_date < this.fromDate.valueOf()) { - new_date = this.fromDate.valueOf(); - } + if (new_date < this.from) new_date = this.from; this.date.set(new_date); if (this._onChange) { this._onChange(new_date); diff --git a/src/app/ui/custom-fields/item-search-field.component.ts b/src/app/ui/custom-fields/item-search-field.component.ts index b561c78da..3edee2648 100644 --- a/src/app/ui/custom-fields/item-search-field.component.ts +++ b/src/app/ui/custom-fields/item-search-field.component.ts @@ -32,7 +32,6 @@ import { escapeHtml } from '../../common/general'; import { HashMap } from '../../common/types'; import { IconComponent } from '../icon.component'; import { LoadingLabelDirective } from '../loading-label.directive'; -import { SanitizePipe } from '../pipes/sanitise.pipe'; interface SearchItem { id: string; @@ -144,10 +143,7 @@ interface SearchItem {
-
+
@if (option.notes) { {{ option.notes @@ -196,7 +192,6 @@ interface SearchItem { imports: [ LoadingLabelDirective, MatAutocompleteModule, - SanitizePipe, MatFormFieldModule, MatInputModule, FormsModule, @@ -263,9 +258,7 @@ export class ItemSearchFieldComponent options && options.length > 0 ? options : !min_length || trimmed_query.length >= min_length - ? await query_fn(trimmed_query).catch( - () => [] as T[], - ) + ? await query_fn(trimmed_query).catch(() => [] as T[]) : []; const search = trimmed_query.toLowerCase(); return list.filter((item: T) => @@ -315,7 +308,6 @@ export class ItemSearchFieldComponent } public ngOnChanges(changes: SimpleChanges): void { - if (changes.service) this.search_str.set(''); if (changes.options) this._changed.set(Date.now()); } diff --git a/src/app/ui/custom-fields/object-list-field.component.ts b/src/app/ui/custom-fields/object-list-field.component.ts index 3572397a3..564facf61 100644 --- a/src/app/ui/custom-fields/object-list-field.component.ts +++ b/src/app/ui/custom-fields/object-list-field.component.ts @@ -8,7 +8,6 @@ import { import { MatRippleModule } from '@angular/material/core'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; -import { AsyncHandler } from '../../common/async-handler.class'; import { HashMap } from '../../common/types'; import { IconComponent } from '../icon.component'; import { TranslatePipe } from '../translate.pipe'; @@ -135,10 +134,7 @@ import { TranslatePipe } from '../translate.pipe'; MatInputModule, ], }) -export class ObjectListFieldComponent - extends AsyncHandler - implements ControlValueAccessor -{ +export class ObjectListFieldComponent implements ControlValueAccessor { /** List of fields that can be populated for each object */ public readonly fields = input(undefined); public readonly disabled = signal(false); diff --git a/src/app/ui/custom-fields/rich-text-input.component.ts b/src/app/ui/custom-fields/rich-text-input.component.ts deleted file mode 100644 index 021a6ed3c..000000000 --- a/src/app/ui/custom-fields/rich-text-input.component.ts +++ /dev/null @@ -1,251 +0,0 @@ -import { - AfterViewInit, - Component, - ElementRef, - forwardRef, - input, - OnChanges, - signal, - SimpleChanges, - viewChild, -} from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { apiKey, token } from '@placeos/ts-client'; - -import SunEditor from 'suneditor'; -import { font, fontSize, formatBlock, link, list } from 'suneditor/src/plugins'; -import { AsyncHandler } from '../../common/async-handler.class'; -import { escapeHtml } from '../../common/general'; -import { uploadFile } from '../../common/uploads'; - -@Component({ - selector: 'rich-text-input', - template: ` -
-
-
- `, - styles: [ - ` - :host { - display: block; - position: relative; - min-height: 8rem; - margin-bottom: 4rem; - z-index: 10; - } - `, - ], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - /* istanbul ignore next */ - useExisting: forwardRef(() => RichTextInputComponent), - multi: true, - }, - ], -}) -export class RichTextInputComponent - extends AsyncHandler - implements ControlValueAccessor, OnChanges, AfterViewInit -{ - public readonly placeholder = input(''); - public readonly readonly = input(false); - public readonly images_allowed = input(false); - public readonly disabled = signal(false); - - private readonly _container_el = - viewChild>('container'); - private readonly _editor_el = - viewChild>('editor'); - - private _editor: ReturnType; - private _onChange: (_: string) => void; - private _onTouch: (_: string) => void; - - public readonly registerOnChange = (fn: (_: string) => void) => - (this._onChange = fn); - public readonly registerOnTouched = (fn: (_: string) => void) => - (this._onTouch = fn); - - public ngOnChanges(changes: SimpleChanges) { - if (changes.images_allowed) { - this.timeout('init', () => this._initialiseEditor()); - } - } - - public ngAfterViewInit() { - this.timeout('init', () => this._initialiseEditor()); - } - - public override ngOnDestroy() { - this._editor?.destroy(); - delete this._editor; - super.ngOnDestroy(); - } - - /** - * Update the form field value - * @param new_value New value to set on the form field - */ - public setValue(new_value: string): void { - if (this.disabled()) return; - /* istanbul ignore else */ - if (this._onChange) this._onChange(new_value); - this._onTouch?.(new_value); - } - - /** - * Update local value when form control value is changed - * @param value The new value for the component - */ - public writeValue(value: string) { - this.timeout('write', () => { - if (this._editor) { - this._setAuth(); - setTimeout(() => { - this._editor?.setContents(value); - }, 100); - } else this.timeout('write', () => this.writeValue(value)); - }); - } - - private _initialiseEditor() { - const _editor_el = this._editor_el()?.nativeElement; - const _container_el = this._container_el()?.nativeElement; - if (!_editor_el || !_container_el) { - return this.timeout('init', () => this._initialiseEditor()); - } - const buttons = [ - ['font'], - ['fontSize'], - ['formatBlock'], - ['bold', 'underline', 'italic'], - ['align', 'list'], - ['link'], - ]; - if (this.images_allowed()) { - buttons.push(['embedImage', 'embedAttachment']); - } - if (this._editor) { - this.unsub('changes'); - this._editor.destroy(); - _editor_el.innerHTML = ''; - delete this._editor; - } - const embedImagePlugin = { - name: 'embedImage', - display: 'command', - title: 'Insert Image', - innerHTML: - '
image
', - add: (_core: unknown, _targetElement: unknown) => { - // Plugin initialization if needed - }, - action: () => this._embedImage(), - }; - - const embedAttachmentPlugin = { - name: 'embedAttachment', - display: 'command', - title: 'Insert Attachment', - innerHTML: - '
attachment
', - add: (_core: unknown, _targetElement: unknown) => { - // Plugin initialization if needed - }, - action: () => this._embedAttachment(), - }; - const box = - this._container_el()?.nativeElement.getBoundingClientRect() || { - top: 0, - left: 0, - width: 0, - height: 0, - }; - this._editor = SunEditor.create(_editor_el, { - placeholder: this.placeholder(), - height: `${box.height}`, - plugins: [ - font, - fontSize, - formatBlock, - list, - link, - embedImagePlugin, - embedAttachmentPlugin, - ], - buttonList: buttons, - }); - this._editor.readOnly(this.readonly() || this.disabled()); - this._editor.onChange = (c) => this.setValue(c); - } - - public setDisabledState(disabled: boolean) { - this.disabled.set(disabled); - this._editor?.readOnly(this.readonly() || disabled); - } - - private _embedImage() { - if (!this._editor || this.disabled()) return; - // Create a File input element - const file_input = document.createElement('input'); - file_input.setAttribute('type', 'file'); - file_input.setAttribute('accept', 'image/*'); - file_input.click(); - - file_input.onchange = () => { - const file = file_input.files[0]; - uploadFile(file, true).subscribe((upload) => { - if (!upload.id) return; - const link = `/api/engine/v2/uploads/${encodeURIComponent(upload.id)}/url`; - this._setAuth(); - setTimeout(() => { - this._editor?.insertHTML( - `${escapeHtml(file.name)}`, - ); - }, 100); - }); - }; - } - - private _embedAttachment() { - if (!this._editor || this.disabled()) return; - // Create a File input element - const file_input = document.createElement('input'); - file_input.setAttribute('type', 'file'); - file_input.click(); - - file_input.onchange = () => { - const file = file_input.files[0]; - uploadFile(file, true).subscribe((upload) => { - if (!upload.id) return; - const link = `/api/engine/v2/uploads/${encodeURIComponent(upload.id)}/url`; - const is_image = file.type.startsWith('image/'); - this._setAuth(); - setTimeout(() => { - if (is_image) { - this._editor?.insertHTML( - `${escapeHtml(file.name)}`, - ); - } else { - this._editor?.insertHTML( - `${escapeHtml(file.name)}`, - ); - } - }, 100); - }); - }; - } - - private _setAuth() { - const tkn = token(); - document.cookie = `${ - tkn === 'x-api-key' - ? 'api-key=' + encodeURIComponent(apiKey()) - : 'bearer_token=' + encodeURIComponent(tkn) - };max-age=30;path=/api/engine/v2/uploads;samesite=strict;${ - location.protocol === 'https:' ? 'secure;' : '' - }`; - } -} diff --git a/src/app/ui/custom-fields/system-exec/function-argument.component.ts b/src/app/ui/custom-fields/system-exec/function-argument.component.ts index a82a87229..f02e024a7 100644 --- a/src/app/ui/custom-fields/system-exec/function-argument.component.ts +++ b/src/app/ui/custom-fields/system-exec/function-argument.component.ts @@ -16,7 +16,6 @@ import { import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { PlaceModuleFunction } from '@placeos/ts-client'; -import { AsyncHandler } from '../../../common/async-handler.class'; import { HashMap } from '../../../common/types'; import { TranslatePipe } from '../../translate.pipe'; @@ -114,7 +113,6 @@ const validateType = (type: string, raw_value: unknown) => { imports: [TranslatePipe, MatFormFieldModule, MatInputModule, FormsModule], }) export class FunctionArgumentComponent - extends AsyncHandler implements OnChanges, ControlValueAccessor { public readonly method = input(undefined); diff --git a/src/app/ui/custom-tooltip.component.ts b/src/app/ui/custom-tooltip.component.ts index 2a7287c15..a3b23645c 100644 --- a/src/app/ui/custom-tooltip.component.ts +++ b/src/app/ui/custom-tooltip.component.ts @@ -11,25 +11,18 @@ import { input, OnChanges, OnDestroy, - signal, SimpleChanges, TemplateRef, Type, viewChild, - WritableSignal, } from '@angular/core'; import { NavigationEnd, Router } from '@angular/router'; import { filter } from 'rxjs'; import { AsyncHandler } from '../common/async-handler.class'; -import { SanitizePipe } from './pipes/sanitise.pipe'; -export class CustomTooltipData { - data: T; - close: () => void; - constructor(d) { - this.data = d.data; - this.close = d.close || (() => null); - } +/** Injected into tooltip content so it can close its tooltip */ +export abstract class CustomTooltipData { + abstract close: () => void; } @Component({ @@ -45,21 +38,16 @@ export class CustomTooltipData { *ngComponentOutlet="component(); injector: injector" /> } - @case ('html') { -
- } @default { - + } }
`, - imports: [PortalModule, SanitizePipe, NgComponentOutlet, NgTemplateOutlet], + imports: [PortalModule, NgComponentOutlet, NgTemplateOutlet], }) -export class CustomTooltipComponent +export class CustomTooltipComponent extends AsyncHandler implements OnChanges, OnDestroy { @@ -73,11 +61,9 @@ export class CustomTooltipComponent /** Vertical position of the rendered overlay */ public readonly yPosition = input<'top' | 'center' | 'bottom'>('top'); /** Content to render in the tooltip */ - public readonly content = input< - TemplateRef | Type | string - >(undefined); - /** Data associated with the tooltip content */ - public readonly data = input(undefined); + public readonly content = input | Type>( + undefined, + ); /** Whether tooltip has a backdrop */ public readonly backdrop = input(true); /** Whether tooltip has a backdrop */ @@ -85,13 +71,13 @@ export class CustomTooltipComponent /** Delay time in milliseconds to close after hover */ public readonly delay = input(0); /** Type of content to render */ - public readonly type: WritableSignal<'template' | 'component' | 'html'> = - signal('template'); + public readonly type = computed(() => + this.content() instanceof TemplateRef ? 'template' : 'component', + ); public readonly template = computed( () => this.content() as TemplateRef, ); public readonly component = computed(() => this.content() as Type); - public readonly html = computed(() => this.content() as string); public injector: Injector; @@ -131,7 +117,6 @@ export class CustomTooltipComponent if (hover && delay) { this.timeout('onclose', () => this.close(), delay); } - this._updateType(); if (this._overlay_ref) this.close(); const _portal = this._portal(); if (!_portal) return; @@ -197,23 +182,12 @@ export class CustomTooltipComponent } } - private _updateType() { - const content = this.content(); - this.type.set( - typeof content === 'string' - ? 'html' - : content instanceof TemplateRef - ? 'template' - : 'component', - ); - } - private _updateInjector() { this.injector = Injector.create({ providers: [ { provide: CustomTooltipData, - useValue: { data: this.data(), close: () => this.close() }, + useValue: { close: () => this.close() }, }, ], parent: this._injector, diff --git a/src/app/ui/debug-output.component.ts b/src/app/ui/debug-output.component.ts index bbf3099ab..b86001a8e 100644 --- a/src/app/ui/debug-output.component.ts +++ b/src/app/ui/debug-output.component.ts @@ -3,7 +3,6 @@ import { ElementRef, Renderer2, inject, - input, signal, viewChild, } from '@angular/core'; @@ -30,7 +29,7 @@ import { TranslatePipe } from './translate.pipe'; (window:resize)="onWindowResize()" >
- @if (can_edit() && canMutate(4)) { + @if (canMutate(4)) { } - - + @if (canCopy()) { + + } + @if (canCopy(true)) { + + } + +
+ @if (loading()) { + + } @else if (display_item()?.id) { + + +
+ + + +
+ } +
+ + @if (bulk()) { + + } + + +
+ @if (debug() && debug_position() === 'below') { + + } +
+ @if (debug() && debug_position() === 'side') { + + } +
+ `, + imports: [ + MatTabNavPanel, + DebugOutputComponent, + IconComponent, + MatRippleModule, + MatTooltipModule, + TranslatePipe, + ItemTablistComponent, + ItemDetailsComponent, + ItemDetailsSkeletonComponent, + ItemSelectionComponent, + ItemSidebarComponent, + SidebarMenuComponent, + RouterModule, + ], +}) +export class ItemShellComponent { + private _service = inject(ActiveItemService); + + /** Route and item type, e.g. `systems` */ + public readonly name = input.required(); + /** Locale key prefix for the item type, e.g. `SYSTEMS` */ + public readonly i18n_key = input.required(); + public readonly tabs = input([]); + /** Item for the details header. Defaults to the active item. */ + public readonly item = input(undefined); + public readonly extra_actions = input([]); + /** Tag filters for the item list */ + public readonly filter_options = input([]); + /** Show the bulk add button */ + public readonly bulk = input(false, { transform: booleanAttribute }); + /** Show the debug console */ + public readonly debug = input(false, { transform: booleanAttribute }); + + public readonly open_menu = signal(false); + public readonly scroll = signal(0); + public readonly loading = this._service.loading; + public readonly display_item = computed( + () => this.item() ?? (this._service.item() as unknown as DisplayItem), + ); + public readonly debug_position = inject(PlaceDebugService).position; + public readonly canCreate = () => this._service.canMutate(2); + public readonly newItem = () => this._service.create(); + public readonly bulkAdd = () => this._service.bulkAdd(); +} diff --git a/src/app/ui/item-sidebar.component.ts b/src/app/ui/item-sidebar.component.ts index 32e6f0cb4..dafdb2cb9 100644 --- a/src/app/ui/item-sidebar.component.ts +++ b/src/app/ui/item-sidebar.component.ts @@ -11,6 +11,7 @@ import { signal, viewChild, } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; import { MatRippleModule } from '@angular/material/core'; import { MatFormFieldModule } from '@angular/material/form-field'; @@ -31,7 +32,6 @@ import { import { AsyncHandler } from '../common/async-handler.class'; import { ActiveItemService } from '../common/item.service'; import { reportLoadError } from '../common/load-error'; -import { toSignal } from '../common/signals'; import { IconComponent } from './icon.component'; import { LoadingLabelDirective } from './loading-label.directive'; import { SettingsToggleComponent } from './settings-toggle.component'; @@ -207,7 +207,6 @@ interface GroupTreeItem { ? 'page' : null " - (click)="show.set(false)" >

@@ -417,7 +415,6 @@ export class ItemSidebarComponent public readonly title = input('Systems'); public readonly route = input('systems'); public readonly filter_options = input([]); - public readonly show = signal(true); public readonly group_hierarchy = signal([]); /** Whether the group tree is loading */ public readonly group_hierarchy_loading = signal(false); @@ -535,10 +532,6 @@ export class ItemSidebarComponent this._service.setSearch(search_str); } - public trackByFn(item: Record, index: number) { - return item.id || index; - } - public displayName(item: { display_name?: string; name?: string }) { return item.display_name || item.name || ''; } diff --git a/src/app/ui/item-tablist.component.ts b/src/app/ui/item-tablist.component.ts index b847cf7c4..d2a21897a 100644 --- a/src/app/ui/item-tablist.component.ts +++ b/src/app/ui/item-tablist.component.ts @@ -7,6 +7,7 @@ import { model, signal, } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; import { MatTabNavPanel, MatTabsModule } from '@angular/material/tabs'; import { NavigationCancel, @@ -19,7 +20,6 @@ import { } from '@angular/router'; import { AsyncHandler } from '../common/async-handler.class'; import { HotkeysService } from '../common/hotkeys.service'; -import { toSignal } from '../common/signals'; import { ApplicationIcon } from '../common/types'; import { IconComponent } from './icon.component'; diff --git a/src/app/ui/item-timestamps.component.ts b/src/app/ui/item-timestamps.component.ts new file mode 100644 index 000000000..869421737 --- /dev/null +++ b/src/app/ui/item-timestamps.component.ts @@ -0,0 +1,58 @@ +import { DatePipe, NgTemplateOutlet } from '@angular/common'; +import { Component, input } from '@angular/core'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { DateFromPipe } from './pipes/date-from.pipe'; +import { TranslatePipe } from './translate.pipe'; + +/** + * Created and updated rows for an item details grid. Each row is a label cell + * and a value cell, so the host takes no space in the grid. + */ +@Component({ + selector: 'item-timestamps', + template: ` +

+ {{ 'COMMON.CREATED_AT' | translate }} +
+ + @if (updated_at()) { +
+ {{ 'COMMON.UPDATED_AT' | translate }} +
+ + } + +
+ + {{ seconds * 1000 | dateFrom }} + +
+
+ `, + styles: [':host { display: contents; }'], + imports: [ + DatePipe, + DateFromPipe, + MatTooltipModule, + TranslatePipe, + NgTemplateOutlet, + ], +}) +export class ItemTimestampsComponent { + /** Unix time in seconds */ + public readonly created_at = input(0); + /** Unix time in seconds. Hidden when not set. */ + public readonly updated_at = input(0); +} diff --git a/src/app/ui/metadata-display.component.ts b/src/app/ui/metadata-display.component.ts index df401afed..374e879a3 100644 --- a/src/app/ui/metadata-display.component.ts +++ b/src/app/ui/metadata-display.component.ts @@ -22,10 +22,9 @@ import { updateMetadata, } from '@placeos/ts-client'; import { VERSION } from '../../env/version'; -import { escapeHtml } from '../common/general'; -// import { SchemaStateService } from '../admin/schema-state.service'; import { AsyncHandler } from '../common/async-handler.class'; import { describeError, readError } from '../common/errors'; +import { escapeHtml } from '../common/general'; import { notifyError, notifySuccess } from '../common/notifications'; import { HashMap } from '../common/types'; import { currentUser } from '../common/user-state'; @@ -197,7 +196,6 @@ function replaceDescTag(inputString, newContent) { standalone: true, }" lang="json" - [schema]="schema_map()[item.name]" [readonly]="false" /> } @@ -243,7 +241,6 @@ export class MetadataDisplayComponent implements OnChanges { private _dialog = inject(MatDialog); - // private _schemas = inject(SchemaStateService); public readonly item = input(); /** List of metadata associated with the zone */ public readonly metadata = signal([]); @@ -255,8 +252,6 @@ export class MetadataDisplayComponent public readonly loading = signal>({}); /** Whether the metadata list is loading */ public readonly loading_list = signal(false); - /** Map of metadata schemas to the associated metadata */ - public readonly schema_map = signal>({}); /** Metadata contents to view */ public readonly show_view = signal(''); /** Search text for filtering metadata */ @@ -479,22 +474,6 @@ export class MetadataDisplayComponent }; return m; }); - // this.subscription( - // `${group.name}_schema`, - // this.form_map()[ - // group.name - // ].controls.schema.valueChanges.subscribe((_) => { - // let schema = this._schemas.getSchema(_); - // if (!schema) { - // try { - // schema = JSON.parse(_); - // } catch (e) { - // schema = {}; - // } - // } - // this.schema_map[group.name] = schema; - // }), - // ); }); } diff --git a/src/app/ui/pipes/format-list.pipe.ts b/src/app/ui/pipes/format-list.pipe.ts deleted file mode 100644 index d2448793e..000000000 --- a/src/app/ui/pipes/format-list.pipe.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; - -@Pipe({ - name: 'formatList', -}) -export class FormatListPipe implements PipeTransform { - public transform(value: string[]): string { - return value.join('\n'); - } -} diff --git a/src/app/ui/pipes/safe.pipe.ts b/src/app/ui/pipes/safe.pipe.ts index a6c0dbebc..7c0ff391a 100644 --- a/src/app/ui/pipes/safe.pipe.ts +++ b/src/app/ui/pipes/safe.pipe.ts @@ -1,10 +1,8 @@ import { Pipe, PipeTransform, inject } from '@angular/core'; import { DomSanitizer, - SafeHtml, SafeResourceUrl, - SafeScript, - SafeStyle, + SafeUrl, } from '@angular/platform-browser'; @Pipe({ @@ -16,23 +14,14 @@ export class SafePipe implements PipeTransform { /** * Sanitizes the string allowing it to be injected into a template * @param value String to sanitize - * @param type Type of value to sanitise. `resource`, `url`, `script`, `style` or `html` + * @param type Type of value to trust. `resource` or `url` */ public transform( value: string, - type: 'resource' | 'url' | 'script' | 'style' | 'html' = 'html', - ): SafeHtml | SafeResourceUrl | SafeScript | SafeStyle { - switch (type) { - case 'resource': - return this.sanitizer.bypassSecurityTrustResourceUrl(value); - case 'url': - return this.sanitizer.bypassSecurityTrustUrl(value); - case 'script': - return this.sanitizer.bypassSecurityTrustScript(value); - case 'style': - return this.sanitizer.bypassSecurityTrustStyle(value); - default: - return this.sanitizer.bypassSecurityTrustHtml(value); - } + type: 'resource' | 'url', + ): SafeResourceUrl | SafeUrl { + return type === 'resource' + ? this.sanitizer.bypassSecurityTrustResourceUrl(value) + : this.sanitizer.bypassSecurityTrustUrl(value); } } diff --git a/src/app/ui/pipes/sanitise.pipe.ts b/src/app/ui/pipes/sanitise.pipe.ts deleted file mode 100644 index 77eb129c5..000000000 --- a/src/app/ui/pipes/sanitise.pipe.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Pipe, PipeTransform, inject } from '@angular/core'; -import { - DomSanitizer, - SafeHtml, - SafeResourceUrl, - SafeScript, - SafeStyle, -} from '@angular/platform-browser'; - -enum SecurityContext { - NONE = 0, - HTML = 1, - STYLE = 2, - SCRIPT = 3, - URL = 4, - RESOURCE_URL = 5, -} - -@Pipe({ - name: 'sanitize', -}) -export class SanitizePipe implements PipeTransform { - private sanitizer = inject(DomSanitizer); - - transform( - value: unknown, - type: 'resource' | 'url' | 'script' | 'style' | 'html' = 'html', - ): SafeHtml | SafeResourceUrl | SafeScript | SafeStyle | string | null { - switch (type) { - case 'resource': - return this.sanitizer.sanitize( - SecurityContext.RESOURCE_URL, - value, - ); - case 'url': - return this.sanitizer.sanitize(SecurityContext.URL, value); - case 'script': - return this.sanitizer.sanitize(SecurityContext.SCRIPT, value); - case 'style': - return this.sanitizer.sanitize(SecurityContext.STYLE, value); - default: - return this.sanitizer.sanitize(SecurityContext.HTML, value); - } - } -} diff --git a/src/app/ui/sidebar-menu.component.ts b/src/app/ui/sidebar-menu.component.ts index 04be5109e..77a2d1b0b 100644 --- a/src/app/ui/sidebar-menu.component.ts +++ b/src/app/ui/sidebar-menu.component.ts @@ -23,11 +23,11 @@ import { } from '../common/support-access'; import { ApplicationIcon } from '../common/types'; import { BackofficeUsersService } from '../users/users.service'; -import { ApplicationPickerTooltipComponent } from './application-picker-tooltip.component'; import { - CustomTooltipComponent, - CustomTooltipData, -} from './custom-tooltip.component'; + ApplicationPickerTooltipComponent, + isSupportedRedirectUri, +} from './application-picker-tooltip.component'; +import { CustomTooltipComponent } from './custom-tooltip.component'; import { DebugInfoComponent } from './debug-info.component'; import { IconComponent } from './icon.component'; import { SkipToContentComponent } from './skip-to-content.component'; @@ -39,7 +39,6 @@ import { UserMenuTooltipComponent } from './user-menu-tooltip.component'; @Component({ selector: 'sidebar-menu', template: ` -
} - @if (selectable()) { -
- -
- } @for ( column of active_columns(); track column.key; @@ -116,13 +98,7 @@ function sortKey(value: unknown): string { [id]="'column-' + column.key" class="border-base-200 bg-base-300 sticky top-0 z-10 flex min-h-full" [style.gridArea]=" - gridSquare( - 1, - 1 + - i + - (selectable() ? 1 : 0) + - (can_reorder() ? 1 : 0) - ) + gridSquare(1, 1 + i + (can_reorder() ? 1 : 0)) " [attr.aria-sort]=" sorted @@ -196,7 +172,7 @@ function sortKey(value: unknown): string { } @else { @for ( - row of paginated_data(); + row of data_view(); track row['id'] || $index; let i = $index ) { @@ -264,7 +240,7 @@ function sortKey(value: unknown): string { } } } - @if (!paginated_data()?.length) { + @if (!data_view().length) {
} -
- @if (selectable()) { -
- -
- } @for (column of active_columns(); track column; let j = $index) {
> { - // eslint-disable-next-line @angular-eslint/no-input-rename - public readonly selectedInput = input([], { alias: 'selected' }); - public readonly selected = linkedSignal(this.selectedInput); - public readonly data = input>(undefined); public readonly columns = input([]); - public readonly selectable = input(false); - public readonly filter = input(''); public readonly sortable = input(false); public readonly can_reorder = input(false); - public readonly page_size = input(0); /** Fixed row height in pixels. Zero keeps the standard table layout. */ public readonly virtual_row_height = input(0); public readonly virtual_enabled = computed( @@ -412,45 +355,24 @@ export class SimpleTableComponent> { public readonly trackRow = (index: number, row: T) => row['id'] ?? index; public readonly color = input>({}); public readonly empty_message = input('No data to list'); - public readonly filter_on = input([]); - public readonly selectedChange = output(); public readonly enter_row = output(); - public readonly oncontext = output(); public readonly ondrop = output<[number, number]>(); - public readonly page = signal(0); - public readonly total_count = signal(0); - public readonly total_pages = signal(0); - public readonly active_columns = signal([]); - public readonly sort = signal<{ key: string; reverse: boolean } | null>( - null, + public readonly active_columns = computed(() => + this.columns().filter((_) => _.show !== false), ); + /** Sorted column. Bind it to keep the sort when the page closes. */ + public readonly sort = model(null); // Internal signal to hold the actual data array private readonly _data = signal([]); - // Computed data view with filtering and sorting + // Computed data view with sorting. Returned as is when there is no sort. public readonly data_view = computed(() => { - // Returned as is when there is no filter and no sort let data = this._data(); - const filter_str = this.filter()?.toLowerCase(); const current_sort = this.sort(); - // Apply filter - if (filter_str) { - data = data.filter((v) => { - const filter_on = this.filter_on(); - const keys = filter_on.length ? filter_on : Object.keys(v); - return keys.some((key) => { - const value = v[key]; - const cmp_str = `${JSON.stringify(value)}`.toLowerCase(); - return cmp_str.includes(filter_str); - }); - }); - } - - // Apply sort if (current_sort && data.length) { const { key, reverse } = current_sort; const direction = reverse ? -1 : 1; @@ -479,9 +401,7 @@ export class SimpleTableComponent> { }); // Computed column count - public readonly column_count = computed(() => { - return this.active_columns().length + (this.selectable() ? 1 : 0); - }); + public readonly column_count = computed(() => this.active_columns().length); // Computed data length public readonly data_length = computed(() => this._data().length); @@ -579,54 +499,13 @@ export class SimpleTableComponent> { } }); - // Update active columns when columns input changes + // A new sort changes which row sits at each index, so go back to the top effect(() => { - this.active_columns.set( - this.columns().filter((_) => _.show !== false), - ); - }); - - // A new filter or sort changes which row sits at each index, - // so clear the selection and go back to the top. - effect(() => { - this.filter(); this.sort(); - untracked(() => { - this.selected.set([]); - this.page.set(0); - this._viewport()?.scrollToIndex(0); - }); - }); - - // New data (e.g. a poll) keeps the selection and page where possible - effect(() => { - const count = this.data_view().length; - const page_size_value = this.page_size(); - untracked(() => { - const selected = this.selected(); - if (selected.some((i) => i >= count)) { - this.selected.set(selected.filter((i) => i < count)); - } - if (!page_size_value) return; - const pages = Math.ceil(count / page_size_value); - this.total_count.set(count); - this.total_pages.set(pages); - if (this.page() >= pages) this.page.set(Math.max(pages - 1, 0)); - }); + untracked(() => this._viewport()?.scrollToIndex(0)); }); } - // Paginated data view - public readonly paginated_data = computed(() => { - const data = this.data_view(); - const page_size_value = this.page_size(); - if (!page_size_value) return data; - - const start = this.page() * page_size_value; - const end = (this.page() + 1) * page_size_value; - return data.slice(start, end); - }); - // Computed column template public readonly column_template = computed(() => { let template = this.active_columns() @@ -636,7 +515,6 @@ export class SimpleTableComponent> { (this.virtual_enabled() ? 'minmax(0, 1fr)' : 'auto'), ) .join(' '); - template = this.selectable() ? `3.5rem ${template}` : template; template = this.can_reorder() ? `3.5rem ${template}` : template; return template; }); @@ -657,15 +535,6 @@ export class SimpleTableComponent> { return `${row} / ${column} / ${row + 1} / ${column + 1}`; } - public select(index: number, state: boolean) { - const current_selected = this.selected(); - if (state) { - this.selected.set([...current_selected, index]); - } else { - this.selected.set(current_selected.filter((i) => i !== index)); - } - } - /** Move a row up or down with the keyboard, like a drag and drop */ public moveRow(index: number, offset: 1 | -1, event: Event) { event.preventDefault(); @@ -678,11 +547,6 @@ export class SimpleTableComponent> { this.ondrop.emit([index, target]); } - public selectAll(state: boolean) { - const list = this.data_view(); - this.selected.set(state ? list.map((_item, i) => i) : []); - } - public setSort(key: string) { const current_sort = this.sort(); if (!current_sort || current_sort.key !== key) { diff --git a/src/app/ui/virtual-scroll.component.ts b/src/app/ui/virtual-scroll.component.ts index 45a387f62..92bfb78fc 100644 --- a/src/app/ui/virtual-scroll.component.ts +++ b/src/app/ui/virtual-scroll.component.ts @@ -63,7 +63,6 @@ export class VirtualScrollComponent implements AfterViewInit, OnDestroy { public readonly range = signal(0); public readonly offset = signal(0); - public readonly extra_height = signal(0); public readonly offset_start = computed(() => Math.max(this.offset() - this.buffer(), 0), ); @@ -71,7 +70,7 @@ export class VirtualScrollComponent implements AfterViewInit, OnDestroy { () => this.offset_start() + (this.range() + 2 * (this.buffer() || 2)), ); public readonly scroll_area = computed( - () => this.items().length * this.item_size() + this.extra_height(), + () => this.items().length * this.item_size(), ); private _el = inject(ElementRef); diff --git a/src/app/users/user-about.component.ts b/src/app/users/user-about.component.ts index 8bfb9fad8..e4e374f64 100644 --- a/src/app/users/user-about.component.ts +++ b/src/app/users/user-about.component.ts @@ -2,14 +2,12 @@ import { Clipboard } from '@angular/cdk/clipboard'; import { Component, computed, inject, resource } from '@angular/core'; import { PlaceUser, showDomain } from '@placeos/ts-client'; -import { DatePipe } from '@angular/common'; import { MatRippleModule } from '@angular/material/core'; -import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterLink } from '@angular/router'; import { ActiveItemService } from '../common/item.service'; import { i18n } from '../common/locale.service'; import { notifySuccess } from '../common/notifications'; -import { DateFromPipe } from '../ui/pipes/date-from.pipe'; +import { ItemTimestampsComponent } from '../ui/item-timestamps.component'; import { TranslatePipe } from '../ui/translate.pipe'; @Component({ @@ -91,10 +89,7 @@ import { TranslatePipe } from '../ui/translate.pipe';
} -
- {{ 'COMMON.CREATED_AT' | translate }} -
-
- - {{ item.created_at * 1000 | dateFrom }} - -
- @if (item.updated_at) { -
- {{ 'COMMON.UPDATED_AT' | translate }} -
-
- - {{ item.updated_at * 1000 | dateFrom }} - -
- } +
`, @@ -167,12 +132,10 @@ import { TranslatePipe } from '../ui/translate.pipe'; `, ], imports: [ + ItemTimestampsComponent, TranslatePipe, - DateFromPipe, - MatTooltipModule, MatRippleModule, RouterLink, - DatePipe, ], }) export class UserAboutComponent { diff --git a/src/app/users/user-form.component.ts b/src/app/users/user-form.component.ts index a89bd48e5..66174260b 100644 --- a/src/app/users/user-form.component.ts +++ b/src/app/users/user-form.component.ts @@ -2,7 +2,6 @@ import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { Component, EventEmitter, - OnInit, Output, computed, effect, @@ -10,12 +9,11 @@ import { resource, signal, } from '@angular/core'; -import { FormField, form, submit } from '@angular/forms/signals'; +import { FormField, form } from '@angular/forms/signals'; import { PlaceUser, addUser, authority, - cleanObject, queryDomains, updateUser, } from '@placeos/ts-client'; @@ -26,17 +24,15 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; -import { AsyncHandler } from '../common/async-handler.class'; -import { readError } from '../common/errors'; import { addSignalChipItem, - getInvalidSignalFields, + mergeFormItem, + onSaveHotkey, removeSignalChipItem, + submitFormDialog, } from '../common/forms'; -import { HotkeysService } from '../common/hotkeys.service'; import { i18n } from '../common/locale.service'; -import { notifyError, notifySuccess } from '../common/notifications'; -import { DialogEvent, Identity } from '../common/types'; +import { DialogEvent } from '../common/types'; import { FullscreenModalShellComponent } from '../ui/fullscreen-modal-shell.component'; import { IconComponent } from '../ui/icon.component'; import { SettingsToggleComponent } from '../ui/settings-toggle.component'; @@ -410,14 +406,13 @@ import { generateUserFormModel, userFormSchema } from './users.utilities'; FullscreenModalShellComponent, ], }) -export class UserFormComponent extends AsyncHandler implements OnInit { +export class UserFormComponent { public readonly isSubsystemUser = isSubsystemUser; private _dialog_ref = inject>(MatDialogRef); private _data = inject<{ item: PlaceUser; readonly?: string }>( MAT_DIALOG_DATA, ); private readonly _name = 'USERS'; - private _hotkey = inject(HotkeysService); @Output() public event = new EventEmitter(); @@ -460,7 +455,7 @@ export class UserFormComponent extends AsyncHandler implements OnInit { })); constructor() { - super(); + onSaveHotkey(() => this.submit()); effect(() => { const domains = this.domain_list(); if (!this.formModel().authority_id && domains[0]) { @@ -472,62 +467,34 @@ export class UserFormComponent extends AsyncHandler implements OnInit { }); } - public async ngOnInit() { - this.subscription( - 'save_item_key', - this._hotkey.listen(['KeyS'], () => this.submit()), - ); - } - public async submit(): Promise { - await submit(this.form, async () => { - const item = this._data.item; - this.loading.set(i18n(`${this._name}.SAVING`)); - const item_json = item.toJSON ? item.toJSON() : item; - const form_item = ( - item.id - ? cleanObject({ ...item_json, ...this.formModel() }, [ - undefined, - null, - '', - ]) - : { ...item_json, ...this.formModel() } - ) as Identity; - if (isSubsystemUser()) { - for (const key of [ - 'sys_admin', - 'support', - 'groups', - 'login_name', - ]) - delete form_item[key]; - form_item.authority_id = authority()?.id; - } - try { - const _item = await (form_item.id - ? updateUser( - form_item.id as string, - form_item as unknown as PlaceUser, - ) - : addUser(form_item as unknown as PlaceUser)); - this.event.emit({ reason: 'done', metadata: { item: _item } }); - notifySuccess(i18n(`${this._name}.SAVE_SUCCESS`)); - this._dialog_ref.close(); - } catch (err) { - this.loading.set(null); - notifyError( - i18n(`${this._name}.SAVE_ERROR`, { - error: await readError(err), - }), + await submitFormDialog( + this.form, + { + loading: this.loading, + event: this.event, + close: () => this._dialog_ref.close(), + }, + `${this._name}.`, + () => { + const form_item = mergeFormItem( + this._data.item, + this.formModel(), + [undefined, null, ''], ); - } - }); - if (this.form().invalid()) { - return notifyError( - i18n('COMMON.INVALID_FIELDS', { - field_list: getInvalidSignalFields(this.form).join(', '), - }), - ); - } + if (isSubsystemUser()) { + for (const key of [ + 'sys_admin', + 'support', + 'groups', + 'login_name', + ]) + delete form_item[key]; + form_item.authority_id = authority()?.id; + } + const user = form_item as unknown as PlaceUser; + return user.id ? updateUser(user.id, user) : addUser(user); + }, + ); } } diff --git a/src/app/users/user-groups.component.ts b/src/app/users/user-groups.component.ts index b2dfc9768..fc197a7c4 100644 --- a/src/app/users/user-groups.component.ts +++ b/src/app/users/user-groups.component.ts @@ -1,183 +1,17 @@ -import { Component, effect, inject } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { MatRippleModule } from '@angular/material/core'; -import { MatProgressBarModule } from '@angular/material/progress-bar'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { RouterModule } from '@angular/router'; -import { PlaceGroup, PlaceGroupUser, queryGroups } from '@placeos/ts-client'; -import { groupPermissionLabels } from '../groups/group-permissions'; -import { ItemSearchFieldComponent } from '../ui/custom-fields/item-search-field.component'; -import { IconComponent } from '../ui/icon.component'; -import { LoadingLabelDirective } from '../ui/loading-label.directive'; -import { SimpleTableComponent } from '../ui/simple-table.component'; -import { TranslatePipe } from '../ui/translate.pipe'; +import { Component, inject } from '@angular/core'; +import { GroupMembershipComponent } from '../groups/group-membership.component'; import { UsersStateService } from './users-state.service'; @Component({ selector: 'user-groups', template: ` -
-
- - -
-
- - -
-
- -
- - -
- @let labels = permissionLabels(row.permissions); - @for (label of labels; track label) { - - {{ label | translate }} - - } - @if (!labels.length) { - {{ - 'COMMON.DEFAULT' | translate - }} - } -
-
- -
- - -
-
+ `, - styles: [ - ` - :host { - height: 100%; - width: 100%; - } - `, - ], - imports: [ - LoadingLabelDirective, - FormsModule, - IconComponent, - ItemSearchFieldComponent, - MatProgressBarModule, - MatRippleModule, - MatTooltipModule, - RouterModule, - SimpleTableComponent, - TranslatePipe, - ], + imports: [GroupMembershipComponent], }) export class UserGroupsComponent { private _service = inject(UsersStateService); - public readonly groups = this._service.groups; + public readonly members = this._service.group_members; public readonly loading = this._service.loading; - public readonly permissionLabels = groupPermissionLabels; - public readonly addGroup = (group: PlaceGroup) => - this._service.addGroup(group); - public readonly bulkAddGroups = () => - this._service.bulkAddGroups(this._current_groups); - public readonly removeGroup = (row: PlaceGroupUser) => - this._service.removeGroup(row); - public readonly editPermissions = (row: PlaceGroupUser) => - this._service.editGroupPermissions(row); - public readonly query_fn = (_: string) => - queryGroups({ - q: _, - limit: 20, - authority_id: this._service.active_item?.authority_id, - } as Record).then((resp) => resp.data); - public readonly exclude_fn = (group: PlaceGroup, __: string) => { - const authority_id = this._service.active_item?.authority_id; - return ( - !!this._service.active_item && - (!!this._current_groups.find((_) => _.group_id === group.id) || - (!!authority_id && group.authority_id !== authority_id)) - ); - }; - - private _current_groups: PlaceGroupUser[] = []; - - constructor() { - effect(() => (this._current_groups = this.groups())); - } } diff --git a/src/app/users/user-history.component.ts b/src/app/users/user-history.component.ts index a5f777d50..a863dfc39 100644 --- a/src/app/users/user-history.component.ts +++ b/src/app/users/user-history.component.ts @@ -1,9 +1,6 @@ import { Component, effect, inject, signal } from '@angular/core'; -import { PlaceUser } from '@placeos/ts-client'; -import { toSignal } from '../common/signals'; import { DatePipe } from '@angular/common'; -import { AsyncHandler } from '../common/async-handler.class'; import { ActiveItemService } from '../common/item.service'; import { SimpleTableComponent } from '../ui/simple-table.component'; import { TranslatePipe } from '../ui/translate.pipe'; @@ -64,17 +61,14 @@ interface UserLogEntry { ], imports: [TranslatePipe, SimpleTableComponent, DatePipe], }) -export class UserHistoryComponent extends AsyncHandler { +export class UserHistoryComponent { private _service = inject(ActiveItemService); public readonly logs = signal([]); - public readonly item = toSignal(this._service.item, { - initialValue: null as PlaceUser | null, - }); + public readonly item = this._service.item; constructor() { - super(); effect(() => { if (this.item()) this.loadUserLogs(); }); diff --git a/src/app/users/user-metadata.component.ts b/src/app/users/user-metadata.component.ts deleted file mode 100644 index 49e236f45..000000000 --- a/src/app/users/user-metadata.component.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Component, inject } from '@angular/core'; -import { PlaceUser } from '@placeos/ts-client'; -import { ActiveItemService } from '../common/item.service'; -import { MetadataDisplayComponent } from '../ui/metadata-display.component'; - -@Component({ - selector: 'user-metadata', - template: ` -
- @if (item) { - - } -
- `, - styles: [``], - imports: [MetadataDisplayComponent], -}) -export class UserMetadataComponent { - private _service = inject(ActiveItemService); - - public get item(): PlaceUser { - const active_item = this._service.active_item; - return (active_item || { id: '' }) as PlaceUser; - } -} diff --git a/src/app/users/users-state.service.ts b/src/app/users/users-state.service.ts index 54f9b6fe8..7747e57b1 100644 --- a/src/app/users/users-state.service.ts +++ b/src/app/users/users-state.service.ts @@ -18,13 +18,7 @@ import { ActiveItemService } from '../common/item.service'; import { i18n } from '../common/locale.service'; import { notifyError, notifySuccess } from '../common/notifications'; import { openedTabs } from '../common/opened-tabs'; -import { waitForEvent } from '../common/signals'; -import { - GroupBulkAddModalComponent, - GroupBulkAddModalData, - GroupBulkAddResult, -} from '../groups/group-bulk-add-modal.component'; -import { GroupPermissionsModalComponent } from '../groups/group-permissions-modal.component'; +import { groupMembership } from '../groups/group-membership'; import { openConfirmModal } from '../overlays/confirm-modal.component'; @Service() @@ -32,13 +26,11 @@ export class UsersStateService { private _service = inject(ActiveItemService); private _dialog = inject(MatDialog); - /** Set while a bulk add runs */ - private _saving = signal(false); private _change = signal(0); public readonly loading = computed( () => - this._saving() || + this.group_members.saving() || this._metadata_count.isLoading() || this._groups.isLoading(), ); @@ -117,142 +109,36 @@ export class UsersStateService { return this._service.active_item as unknown as PlaceUser; } - public async addGroup(group: PlaceGroup) { - if (!group?.id) return; - try { - await addGroupUser({ - user_id: this.active_item.id, - group_id: group.id, - }); - } catch (error) { - return notifyError( - i18n('USERS.GROUP_ADD_ERROR', { error: describeError(error) }), - ); - } - notifySuccess(i18n('USERS.GROUP_ADD_SUCCESS')); - this.changed(); - } - - public async bulkAddGroups(existing_groups: PlaceGroupUser[] = []) { - const result = await waitForEvent( - this._dialog - .open< - GroupBulkAddModalComponent, - GroupBulkAddModalData, - GroupBulkAddResult - >(GroupBulkAddModalComponent, { - data: { - title: 'USERS.GROUPS_BULK', - placeholder: 'GROUPS.SEARCH', - empty_message: 'USERS.GROUPS_BULK_EMPTY', - query_fn: (query: string) => - queryGroups({ - q: query, - limit: 20, - authority_id: this.active_item?.authority_id, - } as Record).then( - (response) => response.data, - ), - exclude: (group: PlaceGroup) => { - const authority_id = this.active_item?.authority_id; - return ( - !!existing_groups.find( - (_) => _.group_id === group.id, - ) || - (!!authority_id && - group.authority_id !== authority_id) - ); - }, - }, - height: 'auto', - width: 'auto', - maxHeight: 'calc(100vh - 2em)', - maxWidth: 'calc(100vw - 2em)', - }) - .afterClosed(), - ); - const groups = result?.items; - if (!groups?.length) return; - // Only send permissions when set so the backend default applies - const permissions = +result.permissions || 0; - this._saving.set(true); - const results = await Promise.allSettled( - groups.map((group) => - addGroupUser({ - user_id: this.active_item.id, - group_id: group.id, - ...(permissions ? { permissions } : {}), - }), - ), - ); - this._saving.set(false); - const failed = results.filter((_) => _.status === 'rejected').length; - if (failed) { - notifyError(i18n('USERS.GROUPS_BULK_ERROR', { count: failed })); - } - const added = results.length - failed; - if (added) { - notifySuccess(i18n('USERS.GROUPS_BULK_SUCCESS', { count: added })); - } - this.changed(); - } - - public async removeGroup(item: PlaceGroupUser) { - const details = await openConfirmModal( - { - title: i18n('USERS.GROUP_REMOVE'), - content: i18n('USERS.GROUP_REMOVE_MSG', { - name: escapeHtml(item.group?.name || item.group_id), - }), - icon: { type: 'icon', content: 'delete' }, - }, - this._dialog, - ); - if (details.reason !== 'done') return; - details.loading(i18n('USERS.GROUP_REMOVE_LOADING')); - try { - await removeGroupUser(item.user_id, item.group_id); - } catch (error) { - details.close(); - return notifyError( - i18n('USERS.GROUP_REMOVE_ERROR', { - error: describeError(error), - }), - ); - } - details.close(); - notifySuccess(i18n('USERS.GROUP_REMOVE_SUCCESS')); - this.changed(); - } - - public async updateGroup(item: PlaceGroupUser) { - try { - await updateGroupUser(item.user_id, item.group_id, { - permissions: +item.permissions || 0, - }); - } catch (error) { - return notifyError( - i18n('USERS.GROUP_SAVE_ERROR', { error: describeError(error) }), - ); - } - notifySuccess(i18n('USERS.GROUP_SAVE_SUCCESS')); - this.changed(); - } - - public async editGroupPermissions(item: PlaceGroupUser) { - const result = await waitForEvent( - this._dialog - .open(GroupPermissionsModalComponent, { - data: { - title: 'USERS.GROUP_PERMISSIONS', - permissions: item.permissions, - }, - }) - .afterClosed(), - ); - if (!result) return; - await this.updateGroup({ ...item, permissions: result.permissions }); - } + /** Group memberships of the active user */ + public readonly group_members = groupMembership( + this._dialog, + { + one: 'USERS.GROUP', + many: 'USERS.GROUPS', + search: 'GROUPS.SEARCH', + column: 'GROUPS.SINGULAR', + member_key: 'group', + route: 'groups', + no_permissions: 'COMMON.DEFAULT', + rows: () => this.groups(), + authority_id: () => this.active_item?.authority_id, + query: (q, authority_id) => + queryGroups({ q, limit: 20, authority_id } as Record< + string, + unknown + >).then((response) => response.data), + member_id: (row) => row.group_id, + member_name: (row) => row.group?.name || row.group_id, + member_detail: (row) => row.group_id, + item_authority: (group) => group.authority_id, + add: (group_id) => + addGroupUser({ user_id: this.active_item.id, group_id }), + remove: (row) => removeGroupUser(row.user_id, row.group_id), + update: (row, { permissions }) => + updateGroupUser(row.user_id, row.group_id, { permissions }), + changed: () => this.changed(), + }, + ); /** Restore the selected user after confirmation. */ public async revive() { diff --git a/src/app/users/users.component.ts b/src/app/users/users.component.ts index 8a2db7438..be27fbc4c 100644 --- a/src/app/users/users.component.ts +++ b/src/app/users/users.component.ts @@ -1,140 +1,32 @@ -import { Component, computed, inject, signal } from '@angular/core'; -import { MatRippleModule } from '@angular/material/core'; -import { MatTabNavPanel } from '@angular/material/tabs'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { RouterModule } from '@angular/router'; +import { Component, computed, inject } from '@angular/core'; import { PlaceUser } from '@placeos/ts-client'; import { extensionsForItem } from '../common/api'; -import { AsyncHandler } from '../common/async-handler.class'; import { ActiveItemService } from '../common/item.service'; import { i18n } from '../common/locale.service'; -import { toSignal } from '../common/signals'; import { isSubsystemUser } from '../common/support-access'; -import { IconComponent } from '../ui/icon.component'; -import { ItemDetailsSkeletonComponent } from '../ui/item-details-skeleton.component'; -import { ItemDetailsComponent } from '../ui/item-details.component'; -import { ItemSelectionComponent } from '../ui/item-selection.component'; -import { ItemSidebarComponent } from '../ui/item-sidebar.component'; -import { ItemTablistComponent } from '../ui/item-tablist.component'; -import { SidebarMenuComponent } from '../ui/sidebar-menu.component'; -import { TranslatePipe } from '../ui/translate.pipe'; +import { ItemShellComponent } from '../ui/item-shell.component'; import { UsersStateService } from './users-state.service'; @Component({ selector: 'new-users-view', template: ` -
- -
+ `, - styles: [``], - imports: [ - MatTabNavPanel, - IconComponent, - MatRippleModule, - MatTooltipModule, - TranslatePipe, - RouterModule, - ItemTablistComponent, - ItemSidebarComponent, - ItemDetailsComponent, - ItemDetailsSkeletonComponent, - ItemSelectionComponent, - SidebarMenuComponent, - ], + imports: [ItemShellComponent], }) -export class UsersComponent extends AsyncHandler { +export class UsersComponent { protected _service = inject(ActiveItemService); private _state = inject(UsersStateService); public readonly name = 'users'; - public open_menu = false; - public readonly scroll = signal(0); - public readonly loading = toSignal(this._service.loading, { - initialValue: false, - }); - public readonly item = toSignal(this._service.item, { - initialValue: null as PlaceUser | null, - }); - public readonly counts = toSignal(this._state.counts, { - initialValue: {} as { metadata?: number; groups?: number }, - }); + public readonly item = this._service.item; + public readonly counts = this._state.counts; public userActions() { const item = this.item(); @@ -163,9 +55,6 @@ export class UsersComponent extends AsyncHandler { ]; } - public readonly canCreate = () => this._service.canMutate(2); - public readonly newItem = () => this._service.create(); - public readonly bulkAdd = () => this._service.bulkAdd(); public readonly tab_list = computed(() => [ { diff --git a/src/app/users/users.routes.ts b/src/app/users/users.routes.ts index 02204be12..275c84b58 100644 --- a/src/app/users/users.routes.ts +++ b/src/app/users/users.routes.ts @@ -1,5 +1,5 @@ import { Routes } from '@angular/router'; -import { AuthorisedAdminGuard } from '../ui/guards/authorised-admin.guard'; +import { authorisedAdminGuard } from '../ui/guards/authorised.guard'; export const ROUTES: Routes = [ { @@ -17,13 +17,13 @@ export const ROUTES: Routes = [ { path: 'metadata', loadComponent: () => - import('./user-metadata.component').then( - (m) => m.UserMetadataComponent, + import('../ui/item-metadata.component').then( + (m) => m.ItemMetadataComponent, ), }, { path: 'groups', - canActivate: [AuthorisedAdminGuard], + canActivate: [authorisedAdminGuard], loadComponent: () => import('./user-groups.component').then( (m) => m.UserGroupsComponent, @@ -31,7 +31,7 @@ export const ROUTES: Routes = [ }, { path: 'history', - canActivate: [AuthorisedAdminGuard], + canActivate: [authorisedAdminGuard], loadComponent: () => import('./user-history.component').then( (m) => m.UserHistoryComponent, diff --git a/src/app/users/users.service.ts b/src/app/users/users.service.ts index 2feeb84e7..07e719147 100644 --- a/src/app/users/users.service.ts +++ b/src/app/users/users.service.ts @@ -23,8 +23,6 @@ const MAX_LOAD_ATTEMPTS = 12; export class BackofficeUsersService extends AsyncHandler { private _settings = inject(SettingsService); - /** Name for a single user */ - public readonly singular: string = 'user'; /** Signal with the currently available list of users */ public readonly listing = signal([]); @@ -39,7 +37,6 @@ export class BackofficeUsersService extends AsyncHandler { public readonly state = signal(''); public readonly can_create: boolean = false; - public readonly can_edit: boolean = true; /** Whether dark mode is enabled */ public get dark_mode(): boolean { diff --git a/src/app/zones/zone-about.component.ts b/src/app/zones/zone-about.component.ts index 8774c1355..1434bd7d4 100644 --- a/src/app/zones/zone-about.component.ts +++ b/src/app/zones/zone-about.component.ts @@ -10,19 +10,17 @@ import { PlaceSystem, PlaceZone, showZone } from '@placeos/ts-client'; import { ZonesStateService } from './zones-state.service'; -import { AsyncPipe, DatePipe } from '@angular/common'; +import { AsyncPipe } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSelectModule } from '@angular/material/select'; -import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; -import { AsyncHandler } from '../common/async-handler.class'; import { describeError } from '../common/errors'; import { ExecuteMethodFieldComponent } from '../ui/custom-fields/system-exec/execute-method-field.component'; import { SettingsFormComponent } from '../ui/forms/settings-form.component'; +import { ItemTimestampsComponent } from '../ui/item-timestamps.component'; import { LoadingLabelDirective } from '../ui/loading-label.directive'; -import { DateFromPipe } from '../ui/pipes/date-from.pipe'; import { MarkdownPipe } from '../ui/pipes/markdown.pipe'; import { TranslatePipe } from '../ui/translate.pipe'; @@ -140,42 +138,10 @@ import { TranslatePipe } from '../ui/translate.pipe'; }
} -
- {{ 'COMMON.CREATED_AT' | translate }} -
-
- - {{ item().created_at * 1000 | dateFrom }} - -
-
- {{ 'COMMON.UPDATED_AT' | translate }} -
-
- - {{ item()?.updated_at * 1000 | dateFrom }} - -
+
@if (systems().length) { @@ -260,12 +226,11 @@ import { TranslatePipe } from '../ui/translate.pipe'; `, ], imports: [ + ItemTimestampsComponent, LoadingLabelDirective, MatProgressSpinnerModule, SettingsFormComponent, TranslatePipe, - MatTooltipModule, - DateFromPipe, MarkdownPipe, ExecuteMethodFieldComponent, MatFormFieldModule, @@ -273,10 +238,9 @@ import { TranslatePipe } from '../ui/translate.pipe'; FormsModule, RouterModule, AsyncPipe, - DatePipe, ], }) -export class ZoneAboutComponent extends AsyncHandler { +export class ZoneAboutComponent { private _service = inject(ZonesStateService); /** List of associated systems */ @@ -301,7 +265,6 @@ export class ZoneAboutComponent extends AsyncHandler { }); constructor() { - super(); effect(() => { const item = this.item(); this.parent.set(undefined); diff --git a/src/app/zones/zone-children.component.ts b/src/app/zones/zone-children.component.ts index fa1c53f2e..387bcecae 100644 --- a/src/app/zones/zone-children.component.ts +++ b/src/app/zones/zone-children.component.ts @@ -1,5 +1,4 @@ import { Component, computed, inject, model } from '@angular/core'; -import { PlaceZone } from '@placeos/ts-client'; import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; @@ -116,8 +115,4 @@ export class ZoneChildrenComponent { ? zones : zones.filter((sys) => sys.name.toLowerCase().includes(filter)); }); - - public get item(): PlaceZone { - return this._state.active_item as PlaceZone; - } } diff --git a/src/app/zones/zone-form.component.ts b/src/app/zones/zone-form.component.ts index f1a14116d..7a7f0d9f5 100644 --- a/src/app/zones/zone-form.component.ts +++ b/src/app/zones/zone-form.component.ts @@ -9,11 +9,10 @@ import { inject, signal, } from '@angular/core'; -import { FormField, form, submit } from '@angular/forms/signals'; +import { FormField, form } from '@angular/forms/signals'; import { PlaceZone, addZone as addZoneRequest, - cleanObject, showZone, updateZone as updateZoneRequest, } from '@placeos/ts-client'; @@ -25,18 +24,16 @@ import { MatChipInputEvent, MatChipsModule } from '@angular/material/chips'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; -import { AsyncHandler } from '../common/async-handler.class'; -import { readError } from '../common/errors'; import { addSignalChipItem, - getInvalidSignalFields, + mergeFormItem, + onSaveHotkey, removeSignalChipItem, + submitFormDialog, } from '../common/forms'; -import { HotkeysService } from '../common/hotkeys.service'; import { i18n } from '../common/locale.service'; -import { notifyError, notifySuccess } from '../common/notifications'; import { TIMEZONES_IANA } from '../common/timezones'; -import { DialogEvent, Identity } from '../common/types'; +import { DialogEvent } from '../common/types'; import { CounterComponent } from '../ui/counter.component'; import { ImageListFieldComponent } from '../ui/custom-fields/image-list-field.component'; import { ItemSearchFieldComponent } from '../ui/custom-fields/item-search-field.component'; @@ -344,19 +341,18 @@ const TIMEZONE_OPTION_LIMIT = 50; FullscreenModalShellComponent, ], }) -export class ZoneFormComponent extends AsyncHandler implements OnInit { +export class ZoneFormComponent implements OnInit { private _dialog_ref = inject>(MatDialogRef); private _data = inject<{ item: PlaceZone; readonly?: string }>( MAT_DIALOG_DATA, ); - private _hotkey = inject(HotkeysService); private readonly _name = 'ZONES'; @Output() public event = new EventEmitter(); public readonly formModel = signal(generateZoneFormModel(this._data.item)); public readonly form = form(this.formModel, applyZoneFormSchema); - public readonly timezones = TIMEZONES_IANA as string[]; + public readonly timezones = TIMEZONES_IANA; public readonly timezone = computed( () => `${this.formModel().timezone || ''}`, ); @@ -372,7 +368,7 @@ export class ZoneFormComponent extends AsyncHandler implements OnInit { ); constructor() { - super(); + onSaveHotkey(() => this.submit()); effect(() => { // Sync only after the user changes the picker. The parent zone // loads async, so an early save must keep the stored `parent_id`. @@ -414,52 +410,31 @@ export class ZoneFormComponent extends AsyncHandler implements OnInit { public ngOnInit(): void { this.updateZone(); - this.subscription( - 'save_item_key', - this._hotkey.listen(['KeyS'], () => this.submit()), - ); } public async submit(): Promise { - await submit(this.form, async () => { - const item: PlaceZone = this._data.item; - this.loading.set(i18n(`${this._name}.SAVING`)); - const item_json = item.toJSON ? item.toJSON() : item; - // `parent_zone` is UI-only; the backend uses `parent_id`, which the - // effect keeps in sync. Strip it so we never POST `parent_zone: null`. - const { parent_zone: _parent_zone, ...form_value } = - this.formModel(); - const form_item = ( - item.id - ? cleanObject({ ...item_json, ...form_value }, [undefined]) - : { ...item_json, ...form_value } - ) as Identity; - try { - const _item = await (form_item.id - ? updateZoneRequest( - form_item.id as string, - form_item as unknown as PlaceZone, - ) - : addZoneRequest(form_item as unknown as PlaceZone)); - this.event.emit({ reason: 'done', metadata: { item: _item } }); - notifySuccess(i18n(`${this._name}.SAVE_SUCCESS`)); - this._dialog_ref.close(); - } catch (err) { - this.loading.set(null); - notifyError( - i18n(`${this._name}.SAVE_ERROR`, { - error: await readError(err), - }), - ); - } - }); - if (this.form().invalid()) { - return notifyError( - i18n('COMMON.INVALID_FIELDS', { - field_list: getInvalidSignalFields(this.form).join(', '), - }), - ); - } + await submitFormDialog( + this.form, + { + loading: this.loading, + event: this.event, + close: () => this._dialog_ref.close(), + }, + `${this._name}.`, + () => { + // `parent_zone` is UI-only; the backend uses `parent_id`, which the + // effect keeps in sync. Strip it so we never POST `parent_zone: null`. + const { parent_zone: _parent_zone, ...form_value } = + this.formModel(); + const form_item = mergeFormItem( + this._data.item, + form_value, + ) as unknown as PlaceZone; + return form_item.id + ? updateZoneRequest(form_item.id, form_item) + : addZoneRequest(form_item); + }, + ); } /** Show the stored parent zone in the picker. `parent_id` stays as stored */ diff --git a/src/app/zones/zone-groups.component.ts b/src/app/zones/zone-groups.component.ts index 58e6af936..55849eee6 100644 --- a/src/app/zones/zone-groups.component.ts +++ b/src/app/zones/zone-groups.component.ts @@ -1,218 +1,17 @@ -import { Component, effect, inject } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { MatRippleModule } from '@angular/material/core'; -import { MatProgressBarModule } from '@angular/material/progress-bar'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { RouterModule } from '@angular/router'; -import { - authority, - PlaceGroup, - PlaceGroupZone, - queryGroups, -} from '@placeos/ts-client'; -import { groupPermissionLabels } from '../groups/group-permissions'; -import { ItemSearchFieldComponent } from '../ui/custom-fields/item-search-field.component'; -import { IconComponent } from '../ui/icon.component'; -import { LoadingLabelDirective } from '../ui/loading-label.directive'; -import { SimpleTableComponent } from '../ui/simple-table.component'; -import { TranslatePipe } from '../ui/translate.pipe'; +import { Component, inject } from '@angular/core'; +import { GroupMembershipComponent } from '../groups/group-membership.component'; import { ZonesStateService } from './zones-state.service'; @Component({ selector: 'zone-groups', template: ` -
-
- - -
-
- - -
-
- - - - -
- @let labels = permissionLabels(row.permissions); - @for (label of labels; track label) { - - {{ label | translate }} - - } - @if (!labels.length) { - {{ - 'COMMON.DEFAULT' | translate - }} - } -
-
- -
- - {{ - (row.deny ? 'COMMON.TRUE' : 'COMMON.FALSE') | translate - }} - -
-
- -
- - -
-
+ `, - styles: [ - ` - :host { - height: 100%; - width: 100%; - } - `, - ], - imports: [ - LoadingLabelDirective, - FormsModule, - IconComponent, - ItemSearchFieldComponent, - MatProgressBarModule, - MatRippleModule, - MatTooltipModule, - RouterModule, - SimpleTableComponent, - TranslatePipe, - ], + imports: [GroupMembershipComponent], }) export class ZoneGroupsComponent { private _service = inject(ZonesStateService); - public readonly groups = this._service.groups; + public readonly members = this._service.group_members; public readonly loading = this._service.loading; - public readonly permissionLabels = groupPermissionLabels; - public readonly addGroup = (group: PlaceGroup) => - this._service.addGroup(group); - public readonly bulkAddGroups = () => - this._service.bulkAddGroups(this._current_groups); - public readonly removeGroup = (row: PlaceGroupZone) => - this._service.removeGroup(row); - public readonly editPermissions = (row: PlaceGroupZone) => - this._service.editGroupPermissions(row); - public readonly query_fn = (_: string) => - queryGroups({ - q: _, - limit: 20, - authority_id: this.authority_id, - } as Record).then((resp) => resp.data); - public readonly exclude_fn = (group: PlaceGroup, __: string) => { - const authority_id = this.authority_id; - return ( - !!this._service.active_item && - (!!this._current_groups.find((_) => _.group_id === group.id) || - (!!authority_id && group.authority_id !== authority_id)) - ); - }; - - private get authority_id() { - return ( - (this._service.active_item as { authority_id?: string }) - ?.authority_id || - authority()?.id || - undefined - ); - } - - private _current_groups: PlaceGroupZone[] = []; - - constructor() { - effect(() => (this._current_groups = this.groups())); - } } diff --git a/src/app/zones/zone-metadata.component.ts b/src/app/zones/zone-metadata.component.ts deleted file mode 100644 index 8a6ba02e8..000000000 --- a/src/app/zones/zone-metadata.component.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Component, inject } from '@angular/core'; -import { PlaceZone } from '@placeos/ts-client'; -import { ActiveItemService } from '../common/item.service'; -import { MetadataDisplayComponent } from '../ui/metadata-display.component'; - -@Component({ - selector: 'zone-metadata', - template: ` -
- @if (item) { - - } -
- `, - styles: [``], - imports: [MetadataDisplayComponent], -}) -export class ZoneMetadataComponent { - private _service = inject(ActiveItemService); - - public get item(): PlaceZone { - const active_item = this._service.active_item; - return (active_item || { id: '' }) as PlaceZone; - } -} diff --git a/src/app/zones/zone-triggers.component.ts b/src/app/zones/zone-triggers.component.ts index b8b5b3583..054f53ede 100644 --- a/src/app/zones/zone-triggers.component.ts +++ b/src/app/zones/zone-triggers.component.ts @@ -1,5 +1,4 @@ import { Component, computed, inject, model } from '@angular/core'; -import { PlaceZone } from '@placeos/ts-client'; import { FormsModule } from '@angular/forms'; import { MatRippleModule } from '@angular/material/core'; @@ -145,8 +144,4 @@ export class ZoneTriggersComponent { public readonly selectTrigger = () => this._state.selectTrigger(); public readonly deleteTrigger = (t) => this._state.removeTrigger(t); - - public get item(): PlaceZone { - return this._state.active_item as PlaceZone; - } } diff --git a/src/app/zones/zones-state.service.ts b/src/app/zones/zones-state.service.ts index 078275799..981720114 100644 --- a/src/app/zones/zones-state.service.ts +++ b/src/app/zones/zones-state.service.ts @@ -21,23 +21,17 @@ import { describeError, readError } from '../common/errors'; import { escapeHtml, unique } from '../common/general'; import { ActiveItemService } from '../common/item.service'; import { reportLoadError } from '../common/load-error'; -import { i18n } from '../common/locale.service'; import { notifyError, notifySuccess } from '../common/notifications'; import { openedTabs } from '../common/opened-tabs'; -import { waitForEvent } from '../common/signals'; +import { waitForDialog } from '../common/signals'; import { collectPages, isSubsystemUser, querySupportSystems as querySystems, querySupportZones as queryZones, } from '../common/support-access'; -import { DialogEvent, Identity } from '../common/types'; -import { - GroupBulkAddModalComponent, - GroupBulkAddModalData, - GroupBulkAddResult, -} from '../groups/group-bulk-add-modal.component'; -import { GroupPermissionsModalComponent } from '../groups/group-permissions-modal.component'; +import { Identity } from '../common/types'; +import { groupMembership } from '../groups/group-membership'; import { openConfirmModal } from '../overlays/confirm-modal.component'; import { SelectItemModalComponent, @@ -49,13 +43,11 @@ export class ZonesStateService { private _service = inject(ActiveItemService); private _dialog = inject(MatDialog); - /** Set while a bulk add runs */ - private _saving = signal(false); private _change = signal(0); public readonly loading = computed( () => - this._saving() || + this.group_members.saving() || this._systems.isLoading() || this._triggers.isLoading() || this._metadata_count.isLoading() || @@ -228,13 +220,7 @@ export class ZonesStateService { queryTriggers({ q: _ }).then((resp) => resp.data), }, }); - const details = await Promise.race([ - waitForEvent( - ref.componentInstance.event, - (_: DialogEvent) => _.reason === 'action', - ), - waitForEvent(ref.afterClosed()), - ]); + const details = await waitForDialog(ref, 'action'); if (details?.reason !== 'action') return ref.close(); try { const zone = await this.addTrigger( @@ -294,146 +280,40 @@ export class ZonesStateService { if (zone) this._service.replaceItem(zone as unknown as Identity); } - public async addGroup(group: PlaceGroup) { - if (!group?.id) return; - try { - await addGroupZone({ - group_id: group.id, - zone_id: this.active_item.id, - }); - } catch (error) { - return notifyError( - i18n('ZONES.GROUP_ADD_ERROR', { error: describeError(error) }), - ); - } - notifySuccess(i18n('ZONES.GROUP_ADD_SUCCESS')); - this.changed(); - } - - public async bulkAddGroups(existing_groups: PlaceGroupZone[] = []) { - const result = await waitForEvent( - this._dialog - .open< - GroupBulkAddModalComponent, - GroupBulkAddModalData, - GroupBulkAddResult - >(GroupBulkAddModalComponent, { - data: { - title: 'ZONES.GROUPS_BULK', - placeholder: 'GROUPS.SEARCH', - empty_message: 'ZONES.GROUPS_BULK_EMPTY', - query_fn: (query: string) => - queryGroups({ - q: query, - limit: 20, - authority_id: this.authority_id, - } as Record).then( - (response) => response.data, - ), - exclude: (group: PlaceGroup) => { - const authority_id = this.authority_id; - return ( - !!existing_groups.find( - (_) => _.group_id === group.id, - ) || - (!!authority_id && - group.authority_id !== authority_id) - ); - }, - }, - height: 'auto', - width: 'auto', - maxHeight: 'calc(100vh - 2em)', - maxWidth: 'calc(100vw - 2em)', - }) - .afterClosed(), - ); - const groups = result?.items; - if (!groups?.length) return; - this._saving.set(true); - const results = await Promise.allSettled( - groups.map((group) => - addGroupZone({ - group_id: group.id, - zone_id: this.active_item.id, - }), - ), - ); - this._saving.set(false); - const failed = results.filter((_) => _.status === 'rejected').length; - if (failed) { - notifyError(i18n('ZONES.GROUPS_BULK_ERROR', { count: failed })); - } - const added = results.length - failed; - if (added) { - notifySuccess(i18n('ZONES.GROUPS_BULK_SUCCESS', { count: added })); - } - this.changed(); - } - - public async removeGroup(item: PlaceGroupZone) { - const details = await openConfirmModal( - { - title: i18n('ZONES.GROUP_REMOVE'), - content: i18n('ZONES.GROUP_REMOVE_MSG', { - name: escapeHtml(item.group?.name || item.group_id), - }), - icon: { type: 'icon', content: 'delete' }, - }, - this._dialog, - ); - if (details.reason !== 'done') return; - details.loading(i18n('ZONES.GROUP_REMOVE_LOADING')); - try { - await removeGroupZone(item.group_id, item.zone_id); - } catch (error) { - details.close(); - return notifyError( - i18n('ZONES.GROUP_REMOVE_ERROR', { - error: describeError(error), + /** Group memberships of the active zone */ + public readonly group_members = groupMembership( + this._dialog, + { + one: 'ZONES.GROUP', + many: 'ZONES.GROUPS', + search: 'GROUPS.SEARCH', + column: 'GROUPS.SINGULAR', + member_key: 'group', + route: 'groups', + no_permissions: 'COMMON.DEFAULT', + show_deny: true, + rows: () => this.groups(), + authority_id: () => this.authority_id, + query: (q, authority_id) => + queryGroups({ q, limit: 20, authority_id } as Record< + string, + unknown + >).then((response) => response.data), + member_id: (row) => row.group_id, + member_name: (row) => row.group?.name || row.group_id, + member_detail: (row) => row.group_id, + item_authority: (group) => group.authority_id, + add: (group_id) => + addGroupZone({ group_id, zone_id: this.active_item.id }), + remove: (row) => removeGroupZone(row.group_id, row.zone_id), + update: (row, { permissions, deny }) => + updateGroupZone(row.group_id, row.zone_id, { + permissions, + deny, }), - ); - } - details.close(); - notifySuccess(i18n('ZONES.GROUP_REMOVE_SUCCESS')); - this.changed(); - } - - public async updateGroup(item: PlaceGroupZone) { - try { - await updateGroupZone(item.group_id, item.zone_id, { - permissions: +item.permissions || 0, - deny: !!item.deny, - }); - } catch (error) { - return notifyError( - i18n('ZONES.GROUP_SAVE_ERROR', { error: describeError(error) }), - ); - } - notifySuccess(i18n('ZONES.GROUP_SAVE_SUCCESS')); - this.changed(); - } - - public async editGroupPermissions(item: PlaceGroupZone) { - const result = await waitForEvent( - this._dialog - .open(GroupPermissionsModalComponent, { - data: { - title: 'ZONES.GROUP_PERMISSIONS', - permissions: item.permissions, - deny: item.deny, - show_deny: true, - }, - }) - .afterClosed(), - ); - if (!result) return; - await this.updateGroup({ - ...item, - permissions: result.permissions, - deny: result.deny, - }); - } + changed: () => this.changed(), + }, + ); private changed() { this._change.set(Date.now()); diff --git a/src/app/zones/zones.component.ts b/src/app/zones/zones.component.ts index d31fe6212..02ab9c2ea 100644 --- a/src/app/zones/zones.component.ts +++ b/src/app/zones/zones.component.ts @@ -1,219 +1,100 @@ -import { Component, computed, inject, resource, signal } from '@angular/core'; -import { MatRippleModule } from '@angular/material/core'; -import { MatTabNavPanel } from '@angular/material/tabs'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { ActivatedRoute, Router, RouterModule } from '@angular/router'; +import { Component, computed, inject, resource } from '@angular/core'; import { listZoneTags, PlaceZone } from '@placeos/ts-client'; import { extensionsForItem } from '../common/api'; -import { AsyncHandler } from '../common/async-handler.class'; -import { PlaceDebugService } from '../common/debug.service'; import { ActiveItemService } from '../common/item.service'; import { i18n } from '../common/locale.service'; import { isSubsystemUser } from '../common/support-access'; -import { DebugOutputComponent } from '../ui/debug-output.component'; -import { IconComponent } from '../ui/icon.component'; -import { ItemDetailsSkeletonComponent } from '../ui/item-details-skeleton.component'; -import { ItemDetailsComponent } from '../ui/item-details.component'; -import { ItemSelectionComponent } from '../ui/item-selection.component'; -import { ItemSidebarComponent } from '../ui/item-sidebar.component'; -import { ItemTab, ItemTablistComponent } from '../ui/item-tablist.component'; -import { SidebarMenuComponent } from '../ui/sidebar-menu.component'; -import { TranslatePipe } from '../ui/translate.pipe'; +import { ItemShellComponent } from '../ui/item-shell.component'; +import { ItemTab } from '../ui/item-tablist.component'; import { BackofficeUsersService } from '../users/users.service'; import { ZonesStateService } from './zones-state.service'; @Component({ selector: 'new-zones-view', template: ` -
- -
-
-
- @if (debug_position() === 'below') { - - } -
- @if (debug_position() === 'side') { - - } -
+ `, - styles: [``], - imports: [ - MatTabNavPanel, - DebugOutputComponent, - IconComponent, - TranslatePipe, - MatTooltipModule, - RouterModule, - MatRippleModule, - ItemTablistComponent, - ItemDetailsComponent, - ItemDetailsSkeletonComponent, - ItemSidebarComponent, - SidebarMenuComponent, - ItemSelectionComponent, - ], + imports: [ItemShellComponent], }) -export class ZonesComponent extends AsyncHandler { +export class ZonesComponent { protected _service = inject(ZonesStateService); protected _item = inject(ActiveItemService); - protected _route = inject(ActivatedRoute); - protected _router = inject(Router); - private _debug = inject(PlaceDebugService); private _users = inject(BackofficeUsersService); public readonly item = computed( () => this._item.active_item$() as PlaceZone | null, ); - public readonly loading = this._item.loading; public readonly name = 'zones'; - public readonly open_menu = signal(false); - public readonly scroll = signal(0); - public readonly debug_position = this._debug.position; public readonly counts = this._service.counts; public readonly tab_list = computed(() => { const details = this.counts(); const is_admin = !!this._users.user()?.sys_admin; return ( - [ - { - id: 'about', - name: i18n('ZONES.TAB_ABOUT'), - icon: { content: 'info' }, - }, - { - id: 'systems', - name: i18n('ZONES.TAB_SYSTEMS'), - count: details.systems ?? '?', - icon: { content: 'meeting_room' }, - }, - { - id: 'triggers', - name: i18n('ZONES.TAB_TRIGGERS'), - count: details.triggers ?? '?', - icon: { content: 'timer' }, - }, - { - id: 'metadata', - name: i18n('ZONES.TAB_METADATA'), - count: details.metadata ?? '?', - icon: { content: 'code_blocks' }, - }, - { - id: 'groups', - name: i18n('ZONES.TAB_GROUPS'), - count: details.groups ?? '?', - icon: { content: 'groups' }, - }, - { - id: 'children', - name: i18n('ZONES.TAB_CHILDREN'), - count: details.children ?? '?', - icon: { content: 'account_tree' }, - }, - { - id: 'history', - name: i18n('ZONES.TAB_SETTINGS_HISTORY'), - icon: { content: 'schedule' }, - }, - ] as ItemTab[] - ) - .concat(extensionsForItem(this.item(), this.name)) - .filter( - (tab) => - !isSubsystemUser() || - !['history', 'triggers', 'groups'].includes(tab.id), + ( + [ + { + id: 'about', + name: i18n('ZONES.TAB_ABOUT'), + icon: { content: 'info' }, + }, + { + id: 'systems', + name: i18n('ZONES.TAB_SYSTEMS'), + count: details.systems ?? '?', + icon: { content: 'meeting_room' }, + }, + { + id: 'triggers', + name: i18n('ZONES.TAB_TRIGGERS'), + count: details.triggers ?? '?', + icon: { content: 'timer' }, + }, + { + id: 'metadata', + name: i18n('ZONES.TAB_METADATA'), + count: details.metadata ?? '?', + icon: { content: 'code_blocks' }, + }, + { + id: 'groups', + name: i18n('ZONES.TAB_GROUPS'), + count: details.groups ?? '?', + icon: { content: 'groups' }, + }, + { + id: 'children', + name: i18n('ZONES.TAB_CHILDREN'), + count: details.children ?? '?', + icon: { content: 'account_tree' }, + }, + { + id: 'history', + name: i18n('ZONES.TAB_SETTINGS_HISTORY'), + icon: { content: 'schedule' }, + }, + ] as ItemTab[] ) - // Groups and history routes are admin only - .filter( - (tab) => is_admin || !['history', 'groups'].includes(tab.id), - ); + .concat(extensionsForItem(this.item(), this.name)) + .filter( + (tab) => + !isSubsystemUser() || + !['history', 'triggers', 'groups'].includes(tab.id), + ) + // Groups and history routes are admin only + .filter( + (tab) => + is_admin || !['history', 'groups'].includes(tab.id), + ) + ); }); - public readonly canCreate = () => this._item.canMutate(2); - public readonly newItem = () => this._item.create(); - public readonly bulkAdd = () => this._item.bulkAdd(); private readonly _zone_tags = resource({ loader: async () => listZoneTags().catch(() => []), }); diff --git a/src/app/zones/zones.routes.ts b/src/app/zones/zones.routes.ts index 60f3f1925..8183f3981 100644 --- a/src/app/zones/zones.routes.ts +++ b/src/app/zones/zones.routes.ts @@ -1,6 +1,8 @@ import { Routes } from '@angular/router'; -import { AuthorisedAdminGuard } from '../ui/guards/authorised-admin.guard'; -import { AuthorisedUserGuard } from '../ui/guards/authorised-user.guard'; +import { + authorisedAdminGuard, + authorisedUserGuard, +} from '../ui/guards/authorised.guard'; export const ROUTES: Routes = [ { @@ -24,7 +26,7 @@ export const ROUTES: Routes = [ }, { path: 'triggers', - canActivate: [AuthorisedUserGuard], + canActivate: [authorisedUserGuard], data: { role_only: true }, loadComponent: () => import('./zone-triggers.component').then( @@ -41,13 +43,13 @@ export const ROUTES: Routes = [ { path: 'metadata', loadComponent: () => - import('./zone-metadata.component').then( - (m) => m.ZoneMetadataComponent, + import('../ui/item-metadata.component').then( + (m) => m.ItemMetadataComponent, ), }, { path: 'groups', - canActivate: [AuthorisedAdminGuard], + canActivate: [authorisedAdminGuard], loadComponent: () => import('./zone-groups.component').then( (m) => m.ZoneGroupsComponent, @@ -62,7 +64,7 @@ export const ROUTES: Routes = [ }, { path: 'history', - canActivate: [AuthorisedAdminGuard], + canActivate: [authorisedAdminGuard], loadComponent: () => import('../ui/settings-history-view.component').then( (m) => m.SettingsHistoryViewComponent, diff --git a/src/app/zones/zones.utilites.ts b/src/app/zones/zones.utilites.ts index a35cabbd8..33ade04bd 100644 --- a/src/app/zones/zones.utilites.ts +++ b/src/app/zones/zones.utilites.ts @@ -1,5 +1,5 @@ -import { PlaceZone } from '@placeos/ts-client'; import { required, SchemaFn } from '@angular/forms/signals'; +import { PlaceZone } from '@placeos/ts-client'; export interface ZoneFormModel { id: string; diff --git a/src/tests/admin/signage-plugins.utilities.spec.ts b/src/tests/admin/signage-plugins.utilities.spec.ts index b77eb29bf..f037081e0 100644 --- a/src/tests/admin/signage-plugins.utilities.spec.ts +++ b/src/tests/admin/signage-plugins.utilities.spec.ts @@ -1,7 +1,6 @@ import type { SignagePlugin } from '@placeos/ts-client'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { - forEachWithLimit, generateSignagePluginFormModel, probeSignagePlugin, } from '../../app/admin/signage-plugins/signage-plugins.utilities'; @@ -21,24 +20,6 @@ describe('signage-plugins.utilities', () => { }); }); - describe('forEachWithLimit', () => { - it('runs every item without exceeding the limit', async () => { - let running = 0; - let max_running = 0; - const done: number[] = []; - await forEachWithLimit([1, 2, 3, 4, 5], 2, async (item) => { - running++; - max_running = Math.max(max_running, running); - await new Promise((resolve) => setTimeout(resolve)); - done.push(item); - running--; - }); - - expect(max_running).toBe(2); - expect(done.sort()).toEqual([1, 2, 3, 4, 5]); - }); - }); - describe('probeSignagePlugin', () => { const loaded = { plugin: { name: 'weather', version: '1.0.0', type: 'widget' }, diff --git a/src/tests/admin/storage.fn.spec.ts b/src/tests/admin/storage.fn.spec.ts index 44bb97a51..ea486feee 100644 --- a/src/tests/admin/storage.fn.spec.ts +++ b/src/tests/admin/storage.fn.spec.ts @@ -25,7 +25,6 @@ import { queryStorage, removeStorage, saveStorage, - showStorage, updateStorage, } from '../../app/admin/storage/storage.fn'; @@ -120,36 +119,6 @@ describe('storage.fn', () => { }); }); - describe('showStorage', () => { - it('should call show with id and path', () => { - showStorage('storage-123'); - expect(client.show).toHaveBeenCalledWith( - expect.objectContaining({ - id: 'storage-123', - path: 'storages', - }), - ); - }); - - it('should pass query_params', () => { - showStorage('storage-123', { q: 'test' }); - expect(client.show).toHaveBeenCalledWith( - expect.objectContaining({ - query_params: { q: 'test' }, - }), - ); - }); - - it('should include process function', () => { - showStorage('storage-123'); - expect(client.show).toHaveBeenCalledWith( - expect.objectContaining({ - fn: expect.any(Function), - }), - ); - }); - }); - describe('updateStorage', () => { it('should call update with id and form_data', () => { updateStorage('storage-123', { bucket_name: 'new-bucket' }); diff --git a/src/tests/common/forms.spec.ts b/src/tests/common/forms.spec.ts index 85b2dc0e5..862e51357 100644 --- a/src/tests/common/forms.spec.ts +++ b/src/tests/common/forms.spec.ts @@ -1,7 +1,11 @@ import type { MatChipEditedEvent } from '@angular/material/chips'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { editSignalChipItem, fieldLabel } from '../../app/common/forms'; +vi.mock('@placeos/ts-client', () => + vi.importActual('@placeos/ts-client/dist/index.es.js'), +); + describe('fieldLabel', () => { it('turns form keys into readable names', () => { expect(fieldLabel('zone')).toBe('Zone'); diff --git a/src/tests/common/general.spec.ts b/src/tests/common/general.spec.ts index 536b9037f..536a63b96 100644 --- a/src/tests/common/general.spec.ts +++ b/src/tests/common/general.spec.ts @@ -1,18 +1,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { copyToClipboard, - csvToJson, - detectIE, downloadFile, escapeHtml, eventToPoint, - flatten, getItemWithKeys, issueDescription, jsonToCsv, log, numberToPosition, - padLength, parseCSV, randomInt, unique, @@ -87,45 +83,6 @@ describe('general.ts utilities', () => { }); }); - describe('detectIE', () => { - const original_user_agent = navigator.userAgent; - - afterEach(() => { - Object.defineProperty(navigator, 'userAgent', { - value: original_user_agent, - writable: true, - }); - }); - - it('should return 0 for non-IE browsers', () => { - expect(detectIE()).toBe(0); - }); - - it('should detect MSIE version', () => { - Object.defineProperty(navigator, 'userAgent', { - value: 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)', - writable: true, - }); - expect(detectIE()).toBe(10); - }); - - it('should detect Trident (IE 11)', () => { - Object.defineProperty(navigator, 'userAgent', { - value: 'Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko', - writable: true, - }); - expect(detectIE()).toBe(11); - }); - - it('should detect Edge', () => { - Object.defineProperty(navigator, 'userAgent', { - value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Edge/12.0', - writable: true, - }); - expect(detectIE()).toBe(12); - }); - }); - describe('getItemWithKeys', () => { it('should get value from nested object', () => { const obj = { a: { b: { c: 'value' } } }; @@ -274,7 +231,7 @@ describe('general.ts utilities', () => { }); }); - describe('parseCSV / csvToJson', () => { + describe('parseCSV', () => { it('should parse simple CSV', () => { const csv = 'name,age\nJohn,30\nJane,25'; const result = parseCSV(csv); @@ -365,10 +322,6 @@ describe('general.ts utilities', () => { expect(parseCSV(jsonToCsv(data))).toEqual(data); expect(parseCSV(jsonToCsv(data, [], '\t'), '\t')).toEqual(data); }); - - it('csvToJson should be alias for parseCSV', () => { - expect(csvToJson).toBe(parseCSV); - }); }); describe('jsonToCsv', () => { @@ -517,62 +470,6 @@ describe('general.ts utilities', () => { }); }); - describe('flatten', () => { - it('should flatten nested arrays', () => { - const arr = [1, [2, 3], [4, [5, 6]]]; - expect(flatten(arr)).toEqual([1, 2, 3, 4, 5, 6]); - }); - - it('should handle empty array', () => { - expect(flatten([])).toEqual([]); - }); - - it('should handle already flat array', () => { - expect(flatten([1, 2, 3])).toEqual([1, 2, 3]); - }); - - it('should handle deeply nested arrays', () => { - const arr = [[[[1]]]]; - expect(flatten(arr)).toEqual([1]); - }); - - it('should preserve order', () => { - const arr = [1, [2, 3], 4]; - expect(flatten(arr)).toEqual([1, 2, 3, 4]); - }); - - it('should handle mixed types', () => { - const arr = [1, ['a', 'b'], [true, false]]; - expect(flatten(arr)).toEqual([1, 'a', 'b', true, false]); - }); - }); - - describe('padLength', () => { - it('should pad number to specified length', () => { - expect(padLength(5, 3)).toBe('005'); - }); - - it('should pad string to specified length', () => { - expect(padLength('5', 3)).toBe('005'); - }); - - it('should not pad if already at length', () => { - expect(padLength(123, 3)).toBe('123'); - }); - - it('should not pad if longer than length', () => { - expect(padLength(12345, 3)).toBe('12345'); - }); - - it('should use custom character', () => { - expect(padLength(5, 3, '*')).toBe('**5'); - }); - - it('should default to length 2', () => { - expect(padLength(5)).toBe('05'); - }); - }); - describe('issueDescription', () => { it('should generate issue template with hash and date', () => { const result = issueDescription('abc123', '2024-01-01'); diff --git a/src/tests/common/timezone-helpers.spec.ts b/src/tests/common/timezone-helpers.spec.ts deleted file mode 100644 index ac59fb203..000000000 --- a/src/tests/common/timezone-helpers.spec.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { - getTimezoneOffsetInMinutes, - getTimezoneOffsetString, -} from '../../app/common/timezone-helpers'; - -describe('timezone-helpers.ts utilities', () => { - describe('getTimezoneOffsetInMinutes', () => { - it('should return 0 for UTC', () => { - expect(getTimezoneOffsetInMinutes('UTC')).toBe(0); - }); - - it('should return the half hour offset for Asia/Kolkata', () => { - expect(getTimezoneOffsetInMinutes('Asia/Kolkata')).toBe(330); - }); - - it('should return a number for western timezones', () => { - const offset = getTimezoneOffsetInMinutes('America/New_York'); - expect(typeof offset).toBe('number'); - }); - - it('should handle daylight saving for Australia/Sydney', () => { - const summer = new Date(Date.UTC(2024, 0, 15)); - const winter = new Date(Date.UTC(2024, 6, 15)); - expect(getTimezoneOffsetInMinutes('Australia/Sydney', summer)).toBe( - 660, - ); - expect(getTimezoneOffsetInMinutes('Australia/Sydney', winter)).toBe( - 600, - ); - }); - - it('should accept custom date parameter', () => { - const winter_date = new Date(2024, 0, 15); // January (winter in northern hemisphere) - const summer_date = new Date(2024, 6, 15); // July (summer in northern hemisphere) - - // These should potentially return different offsets due to DST - const winter_offset = getTimezoneOffsetInMinutes( - 'America/New_York', - winter_date, - ); - const summer_offset = getTimezoneOffsetInMinutes( - 'America/New_York', - summer_date, - ); - - expect(typeof winter_offset).toBe('number'); - expect(typeof summer_offset).toBe('number'); - }); - - it('should throw for invalid timezone', () => { - // Invalid timezone throws RangeError from Intl.DateTimeFormat - expect(() => - getTimezoneOffsetInMinutes('Invalid/Timezone'), - ).toThrow(); - }); - }); - - describe('getTimezoneOffsetString', () => { - it('should return +0000 for UTC', () => { - expect(getTimezoneOffsetString('UTC')).toBe('+0000'); - }); - - it('should include the minutes for Asia/Kolkata', () => { - expect(getTimezoneOffsetString('Asia/Kolkata')).toBe('+0530'); - }); - - it('should return formatted string for western timezones', () => { - const result = getTimezoneOffsetString('America/Los_Angeles'); - expect(result.length).toBe(5); - expect(result).toMatch(/^[+-]\d{4}$/); - }); - - it('should cache results', () => { - // Call twice with same timezone - const first = getTimezoneOffsetString('Europe/London'); - const second = getTimezoneOffsetString('Europe/London'); - expect(first).toBe(second); - }); - - it('should start with + or -', () => { - const result = getTimezoneOffsetString('Asia/Tokyo'); - expect(result[0]).toMatch(/[+-]/); - }); - }); -}); diff --git a/src/tests/common/timezones.spec.ts b/src/tests/common/timezones.spec.ts index f164617d6..97a2edc8e 100644 --- a/src/tests/common/timezones.spec.ts +++ b/src/tests/common/timezones.spec.ts @@ -1,215 +1,10 @@ -import { describe, it, expect } from 'vitest'; -import { TIMEZONES, TIMEZONES_IANA } from '../../app/common/timezones'; - -describe('timezones.ts', () => { - describe('TIMEZONES', () => { - it('should be a non-empty array', () => { - expect(Array.isArray(TIMEZONES)).toBe(true); - expect(TIMEZONES.length).toBeGreaterThan(0); - }); - - it('should have over 80 timezone entries', () => { - expect(TIMEZONES.length).toBeGreaterThan(80); - }); - - it('should have required properties on each timezone', () => { - for (const tz of TIMEZONES) { - expect(tz).toHaveProperty('value'); - expect(tz).toHaveProperty('abbr'); - expect(tz).toHaveProperty('offset'); - expect(tz).toHaveProperty('isdst'); - expect(tz).toHaveProperty('text'); - expect(tz).toHaveProperty('utc'); - } - }); - - it('should have string value property', () => { - for (const tz of TIMEZONES) { - expect(typeof tz.value).toBe('string'); - expect(tz.value.length).toBeGreaterThan(0); - } - }); - - it('should have string abbr property', () => { - for (const tz of TIMEZONES) { - expect(typeof tz.abbr).toBe('string'); - } - }); - - it('should have numeric offset property', () => { - for (const tz of TIMEZONES) { - expect(typeof tz.offset).toBe('number'); - } - }); - - it('should have boolean isdst property', () => { - for (const tz of TIMEZONES) { - expect(typeof tz.isdst).toBe('boolean'); - } - }); - - it('should have string text property with UTC offset', () => { - for (const tz of TIMEZONES) { - expect(typeof tz.text).toBe('string'); - expect(tz.text).toContain('UTC'); - } - }); - - it('should have array utc property', () => { - for (const tz of TIMEZONES) { - expect(Array.isArray(tz.utc)).toBe(true); - } - }); - - it('should have offsets in valid range (-12 to +14)', () => { - for (const tz of TIMEZONES) { - expect(tz.offset).toBeGreaterThanOrEqual(-12); - expect(tz.offset).toBeLessThanOrEqual(14); - } - }); - - it('should include common timezones', () => { - const values = TIMEZONES.map((tz) => tz.value); - expect(values).toContain('UTC'); - expect(values).toContain('Pacific Standard Time'); - expect(values).toContain('Eastern Standard Time'); - expect(values).toContain('GMT Standard Time'); - }); - - it('should include Australia timezones', () => { - const values = TIMEZONES.map((tz) => tz.value); - expect(values).toContain('AUS Eastern Standard Time'); - expect(values).toContain('W. Australia Standard Time'); - }); - - it('should include Asia timezones', () => { - const values = TIMEZONES.map((tz) => tz.value); - expect(values).toContain('China Standard Time'); - expect(values).toContain('Japan Standard Time'); - expect(values).toContain('India Standard Time'); - }); - - it('should include Europe timezones', () => { - const values = TIMEZONES.map((tz) => tz.value); - expect(values).toContain('W. Europe Standard Time'); - expect(values).toContain('Central Europe Standard Time'); - }); - - it('should have fractional offsets for special zones', () => { - const india = TIMEZONES.find( - (tz) => tz.value === 'India Standard Time', - ); - expect(india?.offset).toBe(5.5); - - const nepal = TIMEZONES.find( - (tz) => tz.value === 'Nepal Standard Time', - ); - expect(nepal?.offset).toBe(5.75); - }); - }); - - describe('TIMEZONES_IANA', () => { - it('should be a non-empty array', () => { - expect(Array.isArray(TIMEZONES_IANA)).toBe(true); - expect(TIMEZONES_IANA.length).toBeGreaterThan(0); - }); - - it('should have more than 200 IANA timezone identifiers', () => { - expect(TIMEZONES_IANA.length).toBeGreaterThan(200); - }); - - it('should contain only strings', () => { - for (const tz of TIMEZONES_IANA) { - expect(typeof tz).toBe('string'); - } - }); - - it('should be sorted alphabetically', () => { - const sorted = [...TIMEZONES_IANA].sort(); - expect(TIMEZONES_IANA).toEqual(sorted); - }); - - it('should contain unique values', () => { - const unique_set = new Set(TIMEZONES_IANA); - expect(unique_set.size).toBe(TIMEZONES_IANA.length); - }); - - it('should include common IANA identifiers', () => { - expect(TIMEZONES_IANA).toContain('America/New_York'); - expect(TIMEZONES_IANA).toContain('America/Los_Angeles'); - expect(TIMEZONES_IANA).toContain('Europe/London'); - expect(TIMEZONES_IANA).toContain('Asia/Tokyo'); - expect(TIMEZONES_IANA).toContain('Australia/Sydney'); - }); - - it('should include UTC/Etc identifiers', () => { - const etc_zones = TIMEZONES_IANA.filter((tz) => - tz.startsWith('Etc/'), - ); - expect(etc_zones.length).toBeGreaterThan(0); - }); - - it('should include Pacific identifiers', () => { - const pacific_zones = TIMEZONES_IANA.filter((tz) => - tz.startsWith('Pacific/'), - ); - expect(pacific_zones.length).toBeGreaterThan(0); - }); - - it('should include Indian Ocean identifiers', () => { - const indian_zones = TIMEZONES_IANA.filter((tz) => - tz.startsWith('Indian/'), - ); - expect(indian_zones.length).toBeGreaterThan(0); - }); - - it('should include Antarctica identifiers', () => { - const antarctica_zones = TIMEZONES_IANA.filter((tz) => - tz.startsWith('Antarctica/'), - ); - expect(antarctica_zones.length).toBeGreaterThan(0); - }); - - it('should not contain empty strings', () => { - for (const tz of TIMEZONES_IANA) { - expect(tz.length).toBeGreaterThan(0); - } - }); - - it('should have valid IANA format (region/city or Etc/...)', () => { - for (const tz of TIMEZONES_IANA) { - // Most should contain / (region/city format) - // Some like PST8PDT, EST5EDT are exceptions - const is_standard_format = - tz.includes('/') || /^[A-Z]{3,4}\d?[A-Z]{0,3}$/.test(tz); - expect(is_standard_format).toBe(true); - } - }); - }); - - describe('data consistency', () => { - it('should have TIMEZONES_IANA derived from TIMEZONES utc arrays', () => { - // Count all utc entries in TIMEZONES - let total_utc = 0; - for (const tz of TIMEZONES) { - total_utc += tz.utc.length; - } - - // TIMEZONES_IANA should have unique values, so could be less - expect(TIMEZONES_IANA.length).toBeLessThanOrEqual(total_utc); - }); - - it('should have all IANA identifiers in source TIMEZONES', () => { - const all_utc = new Set(); - for (const tz of TIMEZONES) { - for (const utc of tz.utc) { - all_utc.add(utc); - } - } - - for (const iana of TIMEZONES_IANA) { - expect(all_utc.has(iana)).toBe(true); - } - }); +import { describe, expect, it } from 'vitest'; +import { TIMEZONES_IANA } from '../../app/common/timezones'; + +describe('TIMEZONES_IANA', () => { + it('should list common IANA timezones', () => { + expect(TIMEZONES_IANA).toContain('Australia/Sydney'); + expect(TIMEZONES_IANA).toContain('America/New_York'); + expect(TIMEZONES_IANA).toContain('Europe/London'); }); }); diff --git a/src/tests/groups/group-state.service.spec.ts b/src/tests/groups/group-state.service.spec.ts index 827c1ec4a..cc6e5e0d7 100644 --- a/src/tests/groups/group-state.service.spec.ts +++ b/src/tests/groups/group-state.service.spec.ts @@ -170,14 +170,14 @@ describe('group membership actions', () => { }); it('does not send an add request for an unsaved user', async () => { - await service.addUser(new PlaceUser()); + await service.user_members.add(new PlaceUser()); expect(mocks.addGroupUser).not.toHaveBeenCalled(); }); it('reports a failed add without claiming success', async () => { const error = new Error('Forbidden'); mocks.addGroupUser.mockRejectedValue(error); - await service.addUser(new PlaceUser({ id: 'user-1' })); + await service.user_members.add(new PlaceUser({ id: 'user-1' })); expect(mocks.addGroupUser).toHaveBeenCalledExactlyOnceWith({ group_id: 'group-1', user_id: 'user-1', @@ -196,8 +196,8 @@ describe('group membership actions', () => { close, loading, }); - if (kind === 'user') await service.removeUser(user); - else await service.removeZone(zone); + if (kind === 'user') await service.user_members.remove(user); + else await service.zone_members.remove(zone); expect(mocks.removeGroupUser).not.toHaveBeenCalled(); expect(mocks.removeGroupZone).not.toHaveBeenCalled(); expect(loading).not.toHaveBeenCalled(); @@ -205,7 +205,7 @@ describe('group membership actions', () => { ); it('removes a confirmed user membership with the correct user and group IDs', async () => { - await service.removeUser(user); + await service.user_members.remove(user); expect(mocks.removeGroupUser).toHaveBeenCalledExactlyOnceWith( 'user-1', 'group-1', @@ -219,7 +219,7 @@ describe('group membership actions', () => { it('closes the confirmation and reports a failed zone removal', async () => { const error = new Error('Forbidden'); mocks.removeGroupZone.mockRejectedValue(error); - await service.removeZone(zone); + await service.zone_members.remove(zone); expect(mocks.removeGroupZone).toHaveBeenCalledExactlyOnceWith( 'group-1', 'zone-1', @@ -235,7 +235,7 @@ describe('group membership actions', () => { mocks.open.mockReturnValue({ afterClosed: () => of({ permissions: 0 }), }); - await service.editUserPermissions(user); + await service.user_members.editPermissions(user); expect(mocks.updateGroupUser).toHaveBeenCalledExactlyOnceWith( 'user-1', 'group-1', @@ -247,7 +247,7 @@ describe('group membership actions', () => { mocks.open.mockReturnValue({ afterClosed: () => of({ permissions: 16, deny: false }), }); - await service.editZonePermissions(zone); + await service.zone_members.editPermissions(zone); expect(mocks.updateGroupZone).toHaveBeenCalledExactlyOnceWith( 'group-1', 'zone-1', @@ -256,8 +256,8 @@ describe('group membership actions', () => { }); it('does not save permissions when either dialog is cancelled', async () => { - await service.editUserPermissions(user); - await service.editZonePermissions(zone); + await service.user_members.editPermissions(user); + await service.zone_members.editPermissions(zone); expect(mocks.updateGroupUser).not.toHaveBeenCalled(); expect(mocks.updateGroupZone).not.toHaveBeenCalled(); }); @@ -276,7 +276,7 @@ describe('group membership actions', () => { mocks.addGroupUser .mockResolvedValueOnce(user) .mockRejectedValueOnce(new Error('Forbidden')); - await service.bulkAddUsers(); + await service.user_members.bulkAdd(); expect(mocks.addGroupUser).toHaveBeenCalledTimes(2); expect(mocks.addGroupUser).toHaveBeenCalledWith({ group_id: 'group-1', @@ -295,13 +295,13 @@ describe('group membership actions', () => { }); it('starts bulk user permissions at the group default permissions', async () => { - await service.bulkAddUsers(); + await service.user_members.bulkAdd(); expect(mocks.open.mock.calls[0][1].data.permissions).toBe(5); }); it('does not add memberships after a bulk dialog is cancelled', async () => { - await service.bulkAddUsers(); - await service.bulkAddZones(); + await service.user_members.bulkAdd(); + await service.zone_members.bulkAdd(); expect(mocks.addGroupUser).not.toHaveBeenCalled(); expect(mocks.addGroupZone).not.toHaveBeenCalled(); }); diff --git a/src/tests/modules/module-form.component.spec.ts b/src/tests/modules/module-form.component.spec.ts index 0668d135a..aaee6c928 100644 --- a/src/tests/modules/module-form.component.spec.ts +++ b/src/tests/modules/module-form.component.spec.ts @@ -1,7 +1,6 @@ import { provideZonelessChangeDetection } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { provideNoopAnimations } from '@angular/platform-browser/animations'; import { PlaceDriverRole, type PlaceDriver } from '@placeos/ts-client'; import { EMPTY } from 'rxjs'; import { beforeEach, describe, expect, it, vi } from 'vitest'; @@ -60,7 +59,6 @@ describe('ModuleFormComponent', () => { imports: [ModuleFormComponent], providers: [ provideZonelessChangeDetection(), - provideNoopAnimations(), { provide: MatDialogRef, useValue: { @@ -144,7 +142,6 @@ describe('creating a module', () => { imports: [ModuleFormComponent], providers: [ provideZonelessChangeDetection(), - provideNoopAnimations(), { provide: MatDialogRef, useValue: { diff --git a/src/tests/overlays/confirm-modal.component.spec.ts b/src/tests/overlays/confirm-modal.component.spec.ts index a37f7667d..419d192cd 100644 --- a/src/tests/overlays/confirm-modal.component.spec.ts +++ b/src/tests/overlays/confirm-modal.component.spec.ts @@ -6,7 +6,6 @@ import { MatDialogModule, MatDialogRef, } from '@angular/material/dialog'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { Subject } from 'rxjs'; import { beforeEach, describe, expect, it, vi } from 'vitest'; @@ -64,11 +63,7 @@ describe('ConfirmModalComponent', () => { }; await TestBed.configureTestingModule({ - imports: [ - ConfirmModalComponent, - MatDialogModule, - NoopAnimationsModule, - ], + imports: [ConfirmModalComponent, MatDialogModule], providers: [ { provide: MatDialogRef, useValue: dialog_ref_mock }, { provide: MAT_DIALOG_DATA, useValue: default_data }, @@ -113,11 +108,7 @@ describe('ConfirmModalComponent', () => { it('label the confirm button Delete and style it as an error', async () => { await TestBed.resetTestingModule(); await TestBed.configureTestingModule({ - imports: [ - ConfirmModalComponent, - MatDialogModule, - NoopAnimationsModule, - ], + imports: [ConfirmModalComponent, MatDialogModule], providers: [ { provide: MatDialogRef, useValue: dialog_ref_mock }, { @@ -153,11 +144,7 @@ describe('ConfirmModalComponent', () => { await TestBed.resetTestingModule(); await TestBed.configureTestingModule({ - imports: [ - ConfirmModalComponent, - MatDialogModule, - NoopAnimationsModule, - ], + imports: [ConfirmModalComponent, MatDialogModule], providers: [ { provide: MatDialogRef, useValue: dialog_ref_mock }, { provide: MAT_DIALOG_DATA, useValue: {} }, @@ -206,11 +193,7 @@ describe('ConfirmModalComponent', () => { await TestBed.resetTestingModule(); await TestBed.configureTestingModule({ - imports: [ - ConfirmModalComponent, - MatDialogModule, - NoopAnimationsModule, - ], + imports: [ConfirmModalComponent, MatDialogModule], providers: [ { provide: MatDialogRef, useValue: dialog_ref_mock }, { @@ -266,11 +249,7 @@ describe('ConfirmModalComponent', () => { const build = async (option_overrides = {}) => { await TestBed.resetTestingModule(); await TestBed.configureTestingModule({ - imports: [ - ConfirmModalComponent, - MatDialogModule, - NoopAnimationsModule, - ], + imports: [ConfirmModalComponent, MatDialogModule], providers: [ { provide: MatDialogRef, useValue: dialog_ref_mock }, { diff --git a/src/tests/overlays/duplicate-modal.component.spec.ts b/src/tests/overlays/duplicate-modal.component.spec.ts new file mode 100644 index 000000000..39adac6da --- /dev/null +++ b/src/tests/overlays/duplicate-modal.component.spec.ts @@ -0,0 +1,72 @@ +import { provideZonelessChangeDetection } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { + MAT_DIALOG_DATA, + MatDialogRef, + MatDialogState, +} from '@angular/material/dialog'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { DialogEvent } from '../../app/common/types'; +import { DuplicateModalComponent } from '../../app/overlays/duplicate-modal.component'; + +class Item { + id: string; + name: string; + constructor(data: Partial = {}) { + this.id = data.id || ''; + this.name = data.name || ''; + } +} + +describe('DuplicateModalComponent', () => { + afterEach(() => vi.useRealTimers()); + + it('saves numbered copies without the source id and emits them', async () => { + vi.useFakeTimers(); + const save = vi.fn(async (item: Item) => ({ ...item, id: 'new' })); + TestBed.configureTestingModule({ + imports: [DuplicateModalComponent], + providers: [ + provideZonelessChangeDetection(), + { + provide: MAT_DIALOG_DATA, + useValue: { + item: new Item({ id: 'sys-1', name: 'Room' }), + save, + }, + }, + { + provide: MatDialogRef, + useValue: { + getState: () => MatDialogState.OPEN, + close: vi.fn(), + }, + }, + ], + }); + const component = TestBed.createComponent( + DuplicateModalComponent, + ).componentInstance; + const events: DialogEvent[] = []; + component.event.subscribe((event) => events.push(event)); + component.times = 2; + + await component.duplicate(); + + const saved = save.mock.calls.map(([item]) => item); + expect(saved.map((item) => [item.id, item.name])).toEqual([ + ['', 'Room (1)'], + ['', 'Room (2)'], + ]); + expect(saved[0]).toBeInstanceOf(Item); + expect(events).toEqual([ + { + reason: 'done', + metadata: [ + { id: 'new', name: 'Room (1)' }, + { id: 'new', name: 'Room (2)' }, + ], + }, + ]); + }); +}); diff --git a/src/tests/repositories/repository-form.component.spec.ts b/src/tests/repositories/repository-form.component.spec.ts index d2fb76657..445f3f36a 100644 --- a/src/tests/repositories/repository-form.component.spec.ts +++ b/src/tests/repositories/repository-form.component.spec.ts @@ -1,7 +1,6 @@ import { provideZonelessChangeDetection } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { provideNoopAnimations } from '@angular/platform-browser/animations'; import { PlaceRepositoryType, listRepositoryBranches, @@ -44,7 +43,6 @@ describe('RepositoryFormComponent', () => { imports: [RepositoryFormComponent], providers: [ provideZonelessChangeDetection(), - provideNoopAnimations(), { provide: MatDialogRef, useValue: { diff --git a/src/tests/ui/components/context-menu.component.spec.ts b/src/tests/ui/components/context-menu.component.spec.ts index 238b953b4..dd60616ad 100644 --- a/src/tests/ui/components/context-menu.component.spec.ts +++ b/src/tests/ui/components/context-menu.component.spec.ts @@ -1,5 +1,4 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { ContextMenuComponent } from '../../../app/ui/context-menu.component'; @@ -9,7 +8,7 @@ describe('ContextMenuComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ContextMenuComponent, NoopAnimationsModule], + imports: [ContextMenuComponent], }).compileComponents(); fixture = TestBed.createComponent(ContextMenuComponent); diff --git a/src/tests/ui/components/fullscreen-modal-shell.component.spec.ts b/src/tests/ui/components/fullscreen-modal-shell.component.spec.ts index 9944d8e23..d13d68054 100644 --- a/src/tests/ui/components/fullscreen-modal-shell.component.spec.ts +++ b/src/tests/ui/components/fullscreen-modal-shell.component.spec.ts @@ -9,7 +9,6 @@ import { TestBed } from '@angular/core/testing'; import { MatChipsModule } from '@angular/material/chips'; import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { MatSelectModule } from '@angular/material/select'; -import { provideNoopAnimations } from '@angular/platform-browser/animations'; import { Subject } from 'rxjs'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { openConfirmModal } from '../../../app/overlays/confirm-modal.component'; @@ -65,7 +64,6 @@ describe('FullscreenModalShellComponent', () => { imports: [TestFormComponent], providers: [ provideZonelessChangeDetection(), - provideNoopAnimations(), { provide: MatDialogRef, useValue: ref }, { provide: MatDialog, useValue: {} }, ], diff --git a/src/tests/ui/components/simple-table.component.spec.ts b/src/tests/ui/components/simple-table.component.spec.ts index 8ab0ee5de..de46c92c7 100644 --- a/src/tests/ui/components/simple-table.component.spec.ts +++ b/src/tests/ui/components/simple-table.component.spec.ts @@ -2,7 +2,6 @@ import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { SimpleTableComponent, @@ -21,7 +20,7 @@ describe('SimpleTableComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [SimpleTableComponent, NoopAnimationsModule], + imports: [SimpleTableComponent], }).compileComponents(); fixture = TestBed.createComponent(SimpleTableComponent); @@ -47,7 +46,6 @@ describe('SimpleTableComponent', () => { { key: 'name', name: 'Name' }, ]); fixture.componentRef.setInput('virtual_row_height', 64); - fixture.componentRef.setInput('selectable', true); fixture.detectChanges(); await fixture.whenStable(); const viewport = fixture.debugElement.query( @@ -62,36 +60,10 @@ describe('SimpleTableComponent', () => { expect(rows.length).toBe(12); expect(rows[0].textContent).toContain('User 500'); expect(rows[0].getAttribute('aria-rowindex')).toBe('502'); - rows[0] - .querySelector('input[type="checkbox"]') - ?.click(); - fixture.detectChanges(); - expect(component.selected()).toEqual([500]); component.setSort('name'); fixture.detectChanges(); await fixture.whenStable(); expect(component.data_view()).toHaveLength(1000); - expect(component.selected()).toEqual([]); - }); - - it('keeps the selection when the data refreshes', async () => { - const data = signal([ - { id: '1', name: 'Item 1' }, - { id: '2', name: 'Item 2' }, - ]); - fixture.componentRef.setInput('data', data); - fixture.componentRef.setInput('selectable', true); - fixture.detectChanges(); - await fixture.whenStable(); - component.select(1, true); - data.set([{ id: '1', name: 'Item 1' }, ...data().slice(1)]); - fixture.detectChanges(); - await fixture.whenStable(); - expect(component.selected()).toEqual([1]); - data.set([{ id: '1', name: 'Item 1' }]); - fixture.detectChanges(); - await fixture.whenStable(); - expect(component.selected()).toEqual([]); }); it('emits enter_row once per hovered row', async () => { @@ -191,10 +163,6 @@ describe('SimpleTableComponent', () => { }); describe('default state', () => { - it('should have empty selected array', () => { - expect(component.selected()).toEqual([]); - }); - it('should have undefined data by default', () => { expect(component.data()).toBeUndefined(); }); @@ -203,10 +171,6 @@ describe('SimpleTableComponent', () => { expect(component.columns()).toEqual([]); }); - it('should not be selectable by default', () => { - expect(component.selectable()).toBe(false); - }); - it('should not be sortable by default', () => { expect(component.sortable()).toBe(false); }); @@ -215,10 +179,6 @@ describe('SimpleTableComponent', () => { expect(component.empty_message()).toBe('No data to list'); }); - it('should have page 0 by default', () => { - expect(component.page()).toBe(0); - }); - it('should have null sort by default', () => { expect(component.sort()).toBeNull(); }); @@ -315,60 +275,6 @@ describe('SimpleTableComponent', () => { }); }); - describe('filtering', () => { - const test_columns: TableColumn[] = [ - { key: 'name', name: 'Name' }, - { key: 'category', name: 'Category' }, - ]; - - it('should filter data based on filter string', async () => { - const data = [ - { id: '1', name: 'Apple', category: 'Fruit' }, - { id: '2', name: 'Banana', category: 'Fruit' }, - { id: '3', name: 'Carrot', category: 'Vegetable' }, - ]; - fixture.componentRef.setInput('data', data); - fixture.componentRef.setInput('columns', test_columns); - fixture.componentRef.setInput('filter', 'apple'); - fixture.detectChanges(); - await fixture.whenStable(); - - expect(component.data_view().length).toBe(1); - expect(component.data_view()[0].name).toBe('Apple'); - }); - - it('should filter case-insensitively', async () => { - const data = [ - { id: '1', name: 'APPLE', category: 'Fruit' }, - { id: '2', name: 'banana', category: 'Fruit' }, - ]; - fixture.componentRef.setInput('data', data); - fixture.componentRef.setInput('columns', test_columns); - fixture.componentRef.setInput('filter', 'BANANA'); - fixture.detectChanges(); - await fixture.whenStable(); - - expect(component.data_view().length).toBe(1); - }); - - it('should filter only on specified fields when filter_on is set', async () => { - const data = [ - { id: '1', name: 'Apple', category: 'Fruit' }, - { id: '2', name: 'Fruit Cake', category: 'Dessert' }, - ]; - fixture.componentRef.setInput('data', data); - fixture.componentRef.setInput('columns', test_columns); - fixture.componentRef.setInput('filter', 'Fruit'); - fixture.componentRef.setInput('filter_on', ['category']); - fixture.detectChanges(); - await fixture.whenStable(); - - // Only "Apple" has category "Fruit" - expect(component.data_view().length).toBe(1); - expect(component.data_view()[0].name).toBe('Apple'); - }); - }); - describe('sorting', () => { const test_columns: TableColumn[] = [ { key: 'name', name: 'Name' }, @@ -527,103 +433,6 @@ describe('SimpleTableComponent', () => { }); }); - describe('selection', () => { - const test_columns: TableColumn[] = [{ key: 'name', name: 'Name' }]; - const data = [ - { id: '1', name: 'Item 1' }, - { id: '2', name: 'Item 2' }, - { id: '3', name: 'Item 3' }, - ]; - - it('should select an item', async () => { - fixture.componentRef.setInput('data', data); - fixture.componentRef.setInput('columns', test_columns); - fixture.componentRef.setInput('selectable', true); - fixture.detectChanges(); - await fixture.whenStable(); - - component.select(0, true); - - expect(component.selected()).toEqual([0]); - }); - - it('should unselect an item', async () => { - fixture.componentRef.setInput('data', data); - fixture.componentRef.setInput('columns', test_columns); - fixture.componentRef.setInput('selectable', true); - fixture.detectChanges(); - await fixture.whenStable(); - - component.select(0, true); - component.select(1, true); - component.select(0, false); - - expect(component.selected()).toEqual([1]); - }); - - it('should select all items', async () => { - fixture.componentRef.setInput('data', data); - fixture.componentRef.setInput('columns', test_columns); - fixture.componentRef.setInput('selectable', true); - fixture.detectChanges(); - await fixture.whenStable(); - - component.selectAll(true); - - expect(component.selected()).toEqual([0, 1, 2]); - }); - - it('should unselect all items', async () => { - fixture.componentRef.setInput('data', data); - fixture.componentRef.setInput('columns', test_columns); - fixture.componentRef.setInput('selectable', true); - fixture.detectChanges(); - await fixture.whenStable(); - - component.selectAll(true); - component.selectAll(false); - - expect(component.selected()).toEqual([]); - }); - }); - - describe('pagination', () => { - const test_columns: TableColumn[] = [{ key: 'name', name: 'Name' }]; - - it('should paginate data when page_size is set', async () => { - const data = [ - { id: '1', name: 'Item 1' }, - { id: '2', name: 'Item 2' }, - { id: '3', name: 'Item 3' }, - { id: '4', name: 'Item 4' }, - { id: '5', name: 'Item 5' }, - ]; - fixture.componentRef.setInput('data', data); - fixture.componentRef.setInput('columns', test_columns); - fixture.componentRef.setInput('page_size', 2); - fixture.detectChanges(); - await fixture.whenStable(); - - expect(component.paginated_data().length).toBe(2); - expect(component.total_pages()).toBe(3); - }); - - it('should not paginate when page_size is 0', async () => { - const data = [ - { id: '1', name: 'Item 1' }, - { id: '2', name: 'Item 2' }, - { id: '3', name: 'Item 3' }, - ]; - fixture.componentRef.setInput('data', data); - fixture.componentRef.setInput('columns', test_columns); - fixture.componentRef.setInput('page_size', 0); - fixture.detectChanges(); - await fixture.whenStable(); - - expect(component.paginated_data().length).toBe(3); - }); - }); - describe('column template', () => { it('should generate grid template with column sizes', async () => { const columns: TableColumn[] = [ @@ -649,16 +458,6 @@ describe('SimpleTableComponent', () => { expect(component.column_template()).toBe('auto 100px'); }); - it('should prepend selectable column width', async () => { - const columns: TableColumn[] = [{ key: 'name', name: 'Name' }]; - fixture.componentRef.setInput('columns', columns); - fixture.componentRef.setInput('selectable', true); - fixture.detectChanges(); - await fixture.whenStable(); - - expect(component.column_template()).toBe('3.5rem auto'); - }); - it('should prepend reorder column width', async () => { const columns: TableColumn[] = [{ key: 'name', name: 'Name' }]; fixture.componentRef.setInput('columns', columns); @@ -715,16 +514,6 @@ describe('SimpleTableComponent', () => { expect(component.column_count()).toBe(2); }); - - it('should add 1 for selectable', async () => { - const columns: TableColumn[] = [{ key: 'a', name: 'A' }]; - fixture.componentRef.setInput('columns', columns); - fixture.componentRef.setInput('selectable', true); - fixture.detectChanges(); - await fixture.whenStable(); - - expect(component.column_count()).toBe(2); - }); }); describe('columnType method', () => { diff --git a/src/tests/ui/components/support-group-picker.component.spec.ts b/src/tests/ui/components/support-group-picker.component.spec.ts index 13a7c9071..d09be90ad 100644 --- a/src/tests/ui/components/support-group-picker.component.spec.ts +++ b/src/tests/ui/components/support-group-picker.component.spec.ts @@ -5,8 +5,8 @@ import { provideZonelessChangeDetection, } from '@angular/core'; import { TestBed } from '@angular/core/testing'; +import { MATERIAL_ANIMATIONS } from '@angular/material/core'; import { MatDialog } from '@angular/material/dialog'; -import { provideNoopAnimations } from '@angular/platform-browser/animations'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { SupportGroupPickerComponent } from '../../../app/ui/support-group-picker.component'; import { TranslatePipe } from '../../../app/ui/translate.pipe'; @@ -53,7 +53,10 @@ describe('support group picker', () => { imports: [SupportGroupPickerComponent], providers: [ provideZonelessChangeDetection(), - provideNoopAnimations(), + { + provide: MATERIAL_ANIMATIONS, + useValue: { animationsDisabled: true }, + }, ], }).overrideComponent(SupportGroupPickerComponent, { remove: { imports: [TranslatePipe] }, diff --git a/src/tests/ui/extension-outlet.component.spec.ts b/src/tests/ui/extension-outlet.component.spec.ts index 5d85d1ea9..a10f7eea5 100644 --- a/src/tests/ui/extension-outlet.component.spec.ts +++ b/src/tests/ui/extension-outlet.component.spec.ts @@ -3,6 +3,7 @@ import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, convertToParamMap } from '@angular/router'; import { PlaceResource, updateMetadata } from '@placeos/ts-client'; +import { of } from 'rxjs'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { ActiveItemService } from '../../app/common/item.service'; import { @@ -84,14 +85,9 @@ describe('ExtensionOutletComponent', () => { { provide: ActivatedRoute, useValue: { - queryParamMap: { - subscribe: (next: (value: unknown) => void) => { - next( - convertToParamMap({ embed: EXTENSION_URL }), - ); - return { unsubscribe: () => undefined }; - }, - }, + queryParamMap: of( + convertToParamMap({ embed: EXTENSION_URL }), + ), }, }, { diff --git a/src/tests/ui/guards/authorisation.spec.ts b/src/tests/ui/guards/authorisation.spec.ts index d03a8ad1a..4ac771a1c 100644 --- a/src/tests/ui/guards/authorisation.spec.ts +++ b/src/tests/ui/guards/authorisation.spec.ts @@ -1,9 +1,16 @@ import { signal } from '@angular/core'; import { TestBed } from '@angular/core/testing'; -import { ActivatedRouteSnapshot, provideRouter, Router } from '@angular/router'; +import { + ActivatedRouteSnapshot, + provideRouter, + Router, + RouterStateSnapshot, +} from '@angular/router'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { AuthorisedAdminGuard } from '../../../app/ui/guards/authorised-admin.guard'; -import { AuthorisedUserGuard } from '../../../app/ui/guards/authorised-user.guard'; +import { + authorisedAdminGuard, + authorisedUserGuard, +} from '../../../app/ui/guards/authorised.guard'; import { BackofficeUsersService } from '../../../app/users/users.service'; const subsystem = vi.hoisted(() => ({ allowed: false })); @@ -36,16 +43,20 @@ const roles = [ describe.each([ { name: 'admin routes', - guard: AuthorisedAdminGuard, + guard: authorisedAdminGuard, allowed: [true, false, true, false], }, { name: 'support routes', - guard: AuthorisedUserGuard, + guard: authorisedUserGuard, allowed: [true, true, true, false], }, ])('$name', ({ guard, allowed }) => { const user = signal<{ sys_admin: boolean; support: boolean } | null>(null); + const canActivate = (route: ActivatedRouteSnapshot) => + TestBed.runInInjectionContext(() => + guard(route, {} as RouterStateSnapshot), + ) as Promise; beforeEach(() => { subsystem.allowed = false; @@ -54,7 +65,6 @@ describe.each([ TestBed.configureTestingModule({ providers: [ provideRouter([]), - guard, { provide: BackofficeUsersService, useValue: { user } }, ], }); @@ -68,14 +78,8 @@ describe.each([ for (const [index, role] of roles.entries()) { it(`checks ${role.name} access before activate`, async () => { user.set(role); - const instance = TestBed.inject< - AuthorisedAdminGuard | AuthorisedUserGuard - >(guard); const router = TestBed.inject(Router); - const result = await instance.canActivate( - new ActivatedRouteSnapshot(), - router.routerState.snapshot, - ); + const result = await canActivate(new ActivatedRouteSnapshot()); expect(result).toBe(allowed[index]); if (allowed[index]) expect(router.navigate).not.toHaveBeenCalled(); else @@ -88,48 +92,26 @@ describe.each([ it('admits subsystem members only on subsystem routes', async () => { user.set({ sys_admin: false, support: false }); subsystem.allowed = true; - const instance = TestBed.inject< - AuthorisedAdminGuard | AuthorisedUserGuard - >(guard); - expect( - await instance.canActivate( - new ActivatedRouteSnapshot(), - TestBed.inject(Router).routerState.snapshot, - ), - ).toBe(guard === AuthorisedUserGuard); + expect(await canActivate(new ActivatedRouteSnapshot())).toBe( + guard === authorisedUserGuard, + ); const route = new ActivatedRouteSnapshot(); route.data = { role_only: true }; - expect( - await instance.canActivate( - route, - TestBed.inject(Router).routerState.snapshot, - ), - ).toBe(false); + expect(await canActivate(route)).toBe(false); }); it('requires an explicit route opt-in for admin routes', async () => { - if (guard !== AuthorisedAdminGuard) return; + if (guard !== authorisedAdminGuard) return; user.set({ sys_admin: false, support: false }); subsystem.allowed = true; - const instance = TestBed.inject(AuthorisedAdminGuard); const opted_in = Object.assign(new ActivatedRouteSnapshot(), { routeConfig: { data: { allow_subsystem: true } }, }); - expect( - await instance.canActivate( - opted_in, - TestBed.inject(Router).routerState.snapshot, - ), - ).toBe(true); + expect(await canActivate(opted_in)).toBe(true); // Inherited data is not an opt-in; only the route's own config is const child = new ActivatedRouteSnapshot(); child.data = { allow_subsystem: true }; - expect( - await instance.canActivate( - child, - TestBed.inject(Router).routerState.snapshot, - ), - ).toBe(false); + expect(await canActivate(child)).toBe(false); }); it('waits for the connection and user before deciding access', async () => { @@ -137,14 +119,7 @@ describe.each([ connection.set(false); const router = TestBed.inject(Router); const settled = vi.fn(); - const result = TestBed.inject< - AuthorisedAdminGuard | AuthorisedUserGuard - >(guard) - .canActivate( - new ActivatedRouteSnapshot(), - router.routerState.snapshot, - ) - .then(settled); + const result = canActivate(new ActivatedRouteSnapshot()).then(settled); await vi.advanceTimersByTimeAsync(100); expect(settled).not.toHaveBeenCalled(); connection.set(true); diff --git a/src/tests/ui/pipes/format-list.pipe.spec.ts b/src/tests/ui/pipes/format-list.pipe.spec.ts deleted file mode 100644 index 65f0c9117..000000000 --- a/src/tests/ui/pipes/format-list.pipe.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { FormatListPipe } from '../../../app/ui/pipes/format-list.pipe'; - -describe('FormatListPipe', () => { - let pipe: FormatListPipe; - - beforeEach(() => { - pipe = new FormatListPipe(); - }); - - describe('transform', () => { - it('should join array elements with newlines', () => { - const result = pipe.transform(['item1', 'item2', 'item3']); - expect(result).toBe('item1\nitem2\nitem3'); - }); - - it('should return single item without newline', () => { - const result = pipe.transform(['single']); - expect(result).toBe('single'); - }); - - it('should return empty string for empty array', () => { - const result = pipe.transform([]); - expect(result).toBe(''); - }); - - it('should handle array with empty strings', () => { - const result = pipe.transform(['', 'item', '']); - expect(result).toBe('\nitem\n'); - }); - - it('should handle array with special characters', () => { - const result = pipe.transform(['line\twith\ttabs', 'another line']); - expect(result).toBe('line\twith\ttabs\nanother line'); - }); - }); -}); diff --git a/src/tests/ui/pipes/safe.pipe.spec.ts b/src/tests/ui/pipes/safe.pipe.spec.ts index 8f0f8f149..36686da06 100644 --- a/src/tests/ui/pipes/safe.pipe.spec.ts +++ b/src/tests/ui/pipes/safe.pipe.spec.ts @@ -16,23 +16,6 @@ describe('SafePipe', () => { }); describe('transform', () => { - it('should sanitize as HTML by default', () => { - const result = pipe.transform('
test
'); - // SafeHtml is an object with changingThisBreaksApplicationSecurity property - expect(result).toBeTruthy(); - expect((result as any).changingThisBreaksApplicationSecurity).toBe( - '
test
', - ); - }); - - it('should sanitize as HTML when type is "html"', () => { - const result = pipe.transform('

paragraph

', 'html'); - expect(result).toBeTruthy(); - expect((result as any).changingThisBreaksApplicationSecurity).toBe( - '

paragraph

', - ); - }); - it('should sanitize as URL when type is "url"', () => { const result = pipe.transform('javascript:alert(1)', 'url'); expect(result).toBeTruthy(); @@ -52,39 +35,6 @@ describe('SafePipe', () => { ); }); - it('should sanitize as style when type is "style"', () => { - const result = pipe.transform('color: red;', 'style'); - expect(result).toBeTruthy(); - expect((result as any).changingThisBreaksApplicationSecurity).toBe( - 'color: red;', - ); - }); - - it('should sanitize as script when type is "script"', () => { - const result = pipe.transform('console.log("test")', 'script'); - expect(result).toBeTruthy(); - expect((result as any).changingThisBreaksApplicationSecurity).toBe( - 'console.log("test")', - ); - }); - - it('should handle empty string', () => { - const result = pipe.transform(''); - expect(result).toBeTruthy(); - expect((result as any).changingThisBreaksApplicationSecurity).toBe( - '', - ); - }); - - it('should handle complex HTML', () => { - const html = - '
Content
'; - const result = pipe.transform(html, 'html'); - expect((result as any).changingThisBreaksApplicationSecurity).toBe( - html, - ); - }); - it('should handle blob URLs for resources', () => { const url = 'blob:https://example.com/12345'; const result = pipe.transform(url, 'resource'); diff --git a/src/tests/ui/pipes/sanitise.pipe.spec.ts b/src/tests/ui/pipes/sanitise.pipe.spec.ts deleted file mode 100644 index 317e3e037..000000000 --- a/src/tests/ui/pipes/sanitise.pipe.spec.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { DomSanitizer } from '@angular/platform-browser'; -import { beforeEach, describe, expect, it } from 'vitest'; -import { SanitizePipe } from '../../../app/ui/pipes/sanitise.pipe'; - -describe('SanitizePipe', () => { - let pipe: SanitizePipe; - let sanitizer: DomSanitizer; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [SanitizePipe], - }); - sanitizer = TestBed.inject(DomSanitizer); - pipe = TestBed.inject(SanitizePipe); - }); - - describe('transform', () => { - it('should sanitize as HTML by default', () => { - const result = pipe.transform('
safe content
'); - // DomSanitizer.sanitize returns a string (sanitized) or null - expect(typeof result).toBe('string'); - expect(result).toContain('safe content'); - }); - - it('should sanitize HTML and remove dangerous elements', () => { - const result = pipe.transform( - '
safe
', - 'html', - ); - // Script tags should be removed during sanitization - expect(result).not.toContain('