diff --git a/package-lock.json b/package-lock.json index c2e7e6f9..47057cc5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -462,7 +462,6 @@ "integrity": "sha512-1KocmjmBP0qlKQGRhRGN0MGvLxf1q2KDWbvzn7ZGdQrIDLC/hFJ8YmnOWsPrM9RxiZi0o5BxCCu9D7KlbthxIg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@angular-eslint/bundled-angular-compiler": "21.0.1", "eslint-scope": "^9.0.0" @@ -492,7 +491,6 @@ "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.0.9.tgz", "integrity": "sha512-vKt+HT5louWK0/MMm/D7kLDmpG6/5OeG8FaF4WTHc2ZI7vn6V4UGgYNdfUZFTn6NWsK1I8tykL3+ExCCfn51eg==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -672,7 +670,6 @@ "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.0.6.tgz", "integrity": "sha512-5Gw8mXtKXvcvDMWEciPLRYB6Ja5vsikLAidZsdCEIF6Bc51GmoqT5Tk/Ke+ciCd5Hq9Aco/IcHxT1RC3470lZg==", "license": "MIT", - "peer": true, "dependencies": { "parse5": "^8.0.0", "tslib": "^2.3.0" @@ -807,7 +804,6 @@ "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.0.9.tgz", "integrity": "sha512-yU9D5qgZGGhlhaM9p0YZEcFSMq84sZHKUqOjL5a2+bxpT01gO1ZhoOGeyLccClgjaNALH23J9hCvmv05ufhXlw==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -824,7 +820,6 @@ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.0.9.tgz", "integrity": "sha512-wQVaNWZM/iyIggJ6lExxnJG8ProqNp4fDNCinejnJbjiQVH7oLU57uAD153nqe6lHhz9oHVMtHx4qpPh1h/ptQ==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -838,7 +833,6 @@ "integrity": "sha512-yjXdY6PAw9HR3YcWGiGSwPw7sJmnW8ziCqP+5DKJkHDIAHmTWFyxffmWys+cZRDjRuk6GvBmyWemvEjNyfMVMw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "7.28.4", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -901,7 +895,6 @@ "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.0.9.tgz", "integrity": "sha512-3RAkc0esb5fuerAJcjgv+rdsaOQfjkWIRckyvDFecAFyeouYWn8x3pAp9rDI02PTMnup+qSgZOqPN1mG3BAjgg==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -927,7 +920,6 @@ "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.0.9.tgz", "integrity": "sha512-FyTkjXbdN+jJLhg1YlTtsmwtiyTMT/jWYFFWG9NIRvKTIMEJqkxl3pi8/idhsuzbZmvBVTQf6xOyAY1QnBWQig==", "license": "MIT", - "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "tslib": "^2.3.0" @@ -964,7 +956,6 @@ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.0.9.tgz", "integrity": "sha512-NXOiDAlGXex8O1D6ZsHinZsloA/ngvSqcg/g2IzVUtxM/PRKYIyk6DeRnWXx1QCQmUVh96WbcAnFoF2PUj2Z8A==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -1104,7 +1095,6 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -1384,6 +1374,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=0.1.90" } @@ -1476,7 +1467,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1517,7 +1507,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2536,7 +2525,6 @@ "integrity": "sha512-X7/+dG9SLpSzRkwgG5/xiIzW0oMrV3C0HOa7YHG1WnrLK+vCQHfte4k/T80059YBdei29RBC3s+pSMvPJDU9/A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@inquirer/checkbox": "^4.3.0", "@inquirer/confirm": "^5.1.19", @@ -4892,7 +4880,8 @@ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/@standard-schema/spec": { "version": "1.1.0", @@ -4976,6 +4965,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@types/node": "*" } @@ -5136,7 +5126,6 @@ "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.16.0", "@typescript-eslint/types": "8.16.0", @@ -5180,6 +5169,7 @@ "integrity": "sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.53.1", "@typescript-eslint/types": "^8.53.1", @@ -5234,6 +5224,7 @@ "integrity": "sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -5430,6 +5421,7 @@ "integrity": "sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/types": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1" @@ -5448,6 +5440,7 @@ "integrity": "sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/project-service": "8.53.1", "@typescript-eslint/tsconfig-utils": "8.53.1", @@ -5476,6 +5469,7 @@ "integrity": "sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/types": "8.53.1", "eslint-visitor-keys": "^4.2.1" @@ -5494,6 +5488,7 @@ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -5717,7 +5712,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5868,6 +5862,7 @@ "dev": true, "license": "ISC", "optional": true, + "peer": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -5883,6 +5878,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=8.6" }, @@ -5964,6 +5960,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": "^4.5.0 || >= 5.9" } @@ -6025,6 +6022,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=8" }, @@ -6107,7 +6105,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6661,6 +6658,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "debug": "2.6.9", "finalhandler": "1.1.2", @@ -6678,6 +6676,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "ms": "2.0.0" } @@ -6689,6 +6688,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 0.8" } @@ -6700,6 +6700,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", @@ -6719,7 +6720,8 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/connect/node_modules/on-finished": { "version": "2.3.0", @@ -6728,6 +6730,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "ee-first": "1.1.1" }, @@ -6742,6 +6745,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -6934,7 +6938,8 @@ "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", @@ -6984,6 +6989,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=4.0" } @@ -7062,6 +7068,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" @@ -7084,7 +7091,8 @@ "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/dom-serialize": { "version": "2.2.1", @@ -7093,6 +7101,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "custom-event": "~1.0.0", "ent": "~2.2.0", @@ -7237,6 +7246,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", @@ -7259,6 +7269,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=10.0.0" } @@ -7270,6 +7281,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" @@ -7285,6 +7297,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -7296,6 +7309,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -7310,6 +7324,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -7321,6 +7336,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -7539,7 +7555,6 @@ "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -7855,7 +7870,8 @@ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/eventsource": { "version": "3.0.7", @@ -7903,7 +7919,6 @@ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -7964,7 +7979,8 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/fast-deep-equal": { "version": "3.1.3", @@ -8207,6 +8223,7 @@ ], "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=4.0" }, @@ -8243,6 +8260,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", @@ -8271,7 +8289,8 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true, "license": "ISC", - "optional": true + "optional": true, + "peer": true }, "node_modules/fsevents": { "version": "2.3.3", @@ -8393,6 +8412,7 @@ "dev": true, "license": "ISC", "optional": true, + "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -8435,6 +8455,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -8447,6 +8468,7 @@ "dev": true, "license": "ISC", "optional": true, + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -8524,6 +8546,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -8662,6 +8685,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", @@ -8811,6 +8835,7 @@ "dev": true, "license": "ISC", "optional": true, + "peer": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -8870,6 +8895,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -8976,6 +9002,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", @@ -9016,6 +9043,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 8.0.0" }, @@ -9222,6 +9250,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -9243,6 +9272,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@colors/colors": "1.5.0", "body-parser": "^1.19.0", @@ -9283,6 +9313,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=8" } @@ -9294,6 +9325,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", @@ -9320,6 +9352,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -9332,6 +9365,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -9358,6 +9392,7 @@ "dev": true, "license": "ISC", "optional": true, + "peer": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -9371,6 +9406,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "ms": "2.0.0" } @@ -9381,7 +9417,8 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/karma/node_modules/glob-parent": { "version": "5.1.2", @@ -9390,6 +9427,7 @@ "dev": true, "license": "ISC", "optional": true, + "peer": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -9404,6 +9442,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -9418,6 +9457,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=8" } @@ -9429,6 +9469,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -9440,6 +9481,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -9451,6 +9493,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -9465,6 +9508,7 @@ "dev": true, "license": "ISC", "optional": true, + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -9478,7 +9522,8 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/karma/node_modules/picomatch": { "version": "2.3.1", @@ -9487,6 +9532,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=8.6" }, @@ -9501,6 +9547,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", @@ -9518,6 +9565,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -9532,6 +9580,7 @@ "dev": true, "license": "BSD-3-Clause", "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -9543,6 +9592,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -9559,6 +9609,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -9573,6 +9624,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -9588,6 +9640,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -9607,6 +9660,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -9627,6 +9681,7 @@ "dev": true, "license": "ISC", "optional": true, + "peer": true, "engines": { "node": ">=10" } @@ -9648,7 +9703,6 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "copy-anything": "^2.0.1", "parse-node-version": "^1.0.1", @@ -9741,7 +9795,6 @@ "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cli-truncate": "^5.0.0", "colorette": "^2.0.20", @@ -9961,6 +10014,7 @@ "dev": true, "license": "Apache-2.0", "optional": true, + "peer": true, "dependencies": { "date-format": "^4.0.14", "debug": "^4.3.4", @@ -10141,6 +10195,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "bin": { "mime": "cli.js" }, @@ -10211,6 +10266,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -10375,6 +10431,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -10534,7 +10591,6 @@ "integrity": "sha512-2lMGkmS91FyP+p/Tzmu49hY+p1PDgHBNM+Fce8yrzZo8/EbybNPBYfJnwFfl0lwGmqpYLevH2oh12+ikKCLv9g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.3.0", "@rollup/plugin-json": "^6.1.0", @@ -11208,6 +11264,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -11719,6 +11776,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -11907,7 +11965,6 @@ "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -12017,7 +12074,8 @@ "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/qjobs": { "version": "1.2.0", @@ -12026,6 +12084,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=0.9" } @@ -12126,7 +12185,8 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/resolve": { "version": "1.22.11", @@ -12254,6 +12314,7 @@ "dev": true, "license": "ISC", "optional": true, + "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -12303,7 +12364,6 @@ "integrity": "sha512-y9yUpfQvetAjiDLtNMf1hL9NXchIJgWt6zIKeoB+tCd3npX08Eqfzg60V9DhIGVMtQ0AlMkFw5xa+AQ37zxnAA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -12412,7 +12472,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -12424,6 +12483,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -12783,6 +12843,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", @@ -12803,6 +12864,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "debug": "~4.4.1", "ws": "~8.18.3" @@ -12815,6 +12877,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" @@ -12830,6 +12893,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" @@ -12845,6 +12909,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -12856,6 +12921,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -12870,6 +12936,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -13044,6 +13111,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "date-format": "^4.0.14", "debug": "^4.3.4", @@ -13233,6 +13301,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=14.14" } @@ -13323,8 +13392,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tuf-js": { "version": "4.1.0", @@ -13375,7 +13443,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13405,6 +13472,7 @@ ], "license": "MIT", "optional": true, + "peer": true, "bin": { "ua-parser-js": "script/cli.js" }, @@ -13462,6 +13530,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 4.0.0" } @@ -13534,6 +13603,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 0.4.0" } @@ -13589,7 +13659,6 @@ "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -14149,7 +14218,6 @@ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", @@ -14229,6 +14297,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -14447,6 +14516,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -14588,7 +14658,6 @@ "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/app/app.navigation.ts b/src/app/app.navigation.ts index 34ab9e3e..d50544b5 100644 --- a/src/app/app.navigation.ts +++ b/src/app/app.navigation.ts @@ -38,6 +38,48 @@ const messageBarNavigationItem: NavigationItem = { fullRouterPath: getFullRoutePath(ROUTE_MAP.components.messageBar) } +const asyncResultNavigationItem: NavigationItem = { + label: 'navigation.async_result', + icon: 'fa-solid fa-spinner', + fullRouterPath: getFullRoutePath(ROUTE_MAP.components.asyncResult) +} + +const fileDownloadNavigationItem: NavigationItem = { + label: 'navigation.file_download', + icon: 'fa-solid fa-download', + fullRouterPath: getFullRoutePath(ROUTE_MAP.components.fileDownload) +} + +const draggableDialogNavigationItem: NavigationItem = { + label: 'navigation.draggable_dialog', + icon: 'fa-solid fa-arrows-up-down-left-right', + fullRouterPath: getFullRoutePath(ROUTE_MAP.components.draggableDialog) +} + +const formsDemoNavigationItem: NavigationItem = { + label: 'navigation.forms_demo', + icon: 'fa-solid fa-pen-to-square', + fullRouterPath: getFullRoutePath(ROUTE_MAP.components.formsDemo) +} + +const routeMapNavigationItem: NavigationItem = { + label: 'navigation.route_map', + icon: 'fa-solid fa-route', + fullRouterPath: getFullRoutePath(ROUTE_MAP.components.routeMap) +} + +const signalStoreNavigationItem: NavigationItem = { + label: 'navigation.signal_store', + icon: 'fa-solid fa-database', + fullRouterPath: getFullRoutePath(ROUTE_MAP.components.signalStore) +} + +const utilsDemoNavigationItem: NavigationItem = { + label: 'navigation.utils_demo', + icon: 'fa-solid fa-wrench', + fullRouterPath: getFullRoutePath(ROUTE_MAP.components.utilsDemo) +} + const componentsNavigationItemContainer: NavigationItem = { label: 'navigation.components', icon: 'fa-solid fa-cubes', @@ -46,7 +88,14 @@ const componentsNavigationItemContainer: NavigationItem = { expandableCardNavigationItem, tableNavigationItem, formTableNavigationItem, - messageBarNavigationItem + messageBarNavigationItem, + asyncResultNavigationItem, + fileDownloadNavigationItem, + draggableDialogNavigationItem, + formsDemoNavigationItem, + routeMapNavigationItem, + signalStoreNavigationItem, + utilsDemoNavigationItem ] } @@ -62,6 +111,18 @@ const globalErrorHandlerNavigationItem: NavigationItem = { fullRouterPath: getFullRoutePath(ROUTE_MAP.globalErrorHandler) } +const subscriptionHandlingNavigationItem: NavigationItem = { + label: 'navigation.subscription_handling', + icon: 'fa-solid fa-link-slash', + fullRouterPath: getFullRoutePath(ROUTE_MAP.subscriptionHandling) +} + +const localStorageNavigationItem: NavigationItem = { + label: 'navigation.local_storage', + icon: 'fa-solid fa-hard-drive', + fullRouterPath: getFullRoutePath(ROUTE_MAP.localStorage) +} + const peoplewareWebsiteNavigationItem: NavigationItem = { label: 'navigation.peopleware_website', icon: 'fa-solid fa-earth-europe', @@ -74,6 +135,8 @@ export const getNavigationItems = () => { dashboardNavigationItem, componentsNavigationItemContainer, inMemoryLoggingNavigationItem, + subscriptionHandlingNavigationItem, + localStorageNavigationItem, globalErrorHandlerNavigationItem, peoplewareWebsiteNavigationItem ] diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 4f09af3e..a1f1bf74 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -12,11 +12,20 @@ export const ROUTE_MAP = { expandableCard: defineRoute('expandable-card'), table: defineRoute('table'), formTable: defineRoute('form-table'), - messageBar: defineRoute('message-bar') + messageBar: defineRoute('message-bar'), + asyncResult: defineRoute('async-result'), + fileDownload: defineRoute('file-download'), + draggableDialog: defineRoute('draggable-dialog'), + formsDemo: defineRoute('forms'), + routeMap: defineRoute('route-map'), + signalStore: defineRoute('signal-store'), + utilsDemo: defineRoute('utils') }), dashboardItem: defineRoute('dashboard-item'), globalErrorHandler: defineRoute('global-error-handler'), - inMemoryLogging: defineRoute('in-memory-logging') + inMemoryLogging: defineRoute('in-memory-logging'), + subscriptionHandling: defineRoute('subscription-handling'), + localStorage: defineRoute('local-storage') } export const routes: Routes = [ @@ -43,5 +52,17 @@ export const routes: Routes = [ path: getRouteSegment(ROUTE_MAP.globalErrorHandler), component: GlobalErrorHandlerComponent, title: 'navigation.global_error_handler' + }, + { + path: getRouteSegment(ROUTE_MAP.subscriptionHandling), + loadComponent: () => + import('./subscription-handling-demo/subscription-handling-demo.component').then((m) => m.SubscriptionHandlingDemoComponent), + title: 'navigation.subscription_handling' + }, + { + path: getRouteSegment(ROUTE_MAP.localStorage), + loadComponent: () => + import('./local-storage-demo/local-storage-demo.component').then((m) => m.LocalStorageDemoComponent), + title: 'navigation.local_storage' } ] diff --git a/src/app/async-result-demo/async-result-demo.component.html b/src/app/async-result-demo/async-result-demo.component.html new file mode 100644 index 00000000..6c520704 --- /dev/null +++ b/src/app/async-result-demo/async-result-demo.component.html @@ -0,0 +1,33 @@ +
+

Use the buttons below to switch between AsyncResult states. Demonstrates createSuccessAsyncResult, createFailedAsyncResult, executeAsyncOperation, and isAsyncResult.

+
+ + + + + + +
+ @if (isAsyncResultGuardResult(); as msg) { + @if (msg) { +

isAsyncResult guard: {{ msg }}

+ } + } + + +

Click a button above or "Simulate load" to see content.

+
+ + @if (data.entity; as items) { +
    + @for (item of items; track item.id) { +
  • {{ item.name }}
  • + } +
+ } +
+ +

No items.

+
+
+
diff --git a/src/app/async-result-demo/async-result-demo.component.scss b/src/app/async-result-demo/async-result-demo.component.scss new file mode 100644 index 00000000..7f26ddcb --- /dev/null +++ b/src/app/async-result-demo/async-result-demo.component.scss @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/src/app/async-result-demo/async-result-demo.component.ts b/src/app/async-result-demo/async-result-demo.component.ts new file mode 100644 index 00000000..d9abf327 --- /dev/null +++ b/src/app/async-result-demo/async-result-demo.component.ts @@ -0,0 +1,83 @@ +import { AsyncPipe } from '@angular/common' +import { ChangeDetectionStrategy, Component, signal } from '@angular/core' +import { MatButtonModule } from '@angular/material/button' +import { AsyncResultModule, createFailedAsyncResult, createSuccessAsyncResult, AsyncResult, isAsyncResult } from '@ppwcode/ng-async' +import { BehaviorSubject } from 'rxjs' +import { delay, map, of } from 'rxjs' +import { executeAsyncOperation } from '@ppwcode/ng-async' + +interface DemoItem { + id: number + name: string +} + +@Component({ + selector: 'ppw-async-result-demo', + standalone: true, + imports: [AsyncResultModule, MatButtonModule, AsyncPipe], + templateUrl: './async-result-demo.component.html', + styleUrl: './async-result-demo.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AsyncResultDemoComponent { + readonly asyncResult = signal | null>(null) + readonly isAsyncResultGuardResult = signal('') + + readonly isLoading$ = new BehaviorSubject(false) + + setInitial(): void { + this.asyncResult.set({ + status: 'initial', + entity: null, + filters: null + }) + this.isLoading$.next(false) + } + + setSuccess(): void { + this.asyncResult.set( + createSuccessAsyncResult([ + { id: 1, name: 'Alpha' }, + { id: 2, name: 'Beta' } + ]) + ) + this.isLoading$.next(false) + } + + setEmpty(): void { + this.asyncResult.set(createSuccessAsyncResult(null)) + this.isLoading$.next(false) + } + + setFailed(): void { + this.asyncResult.set( + createFailedAsyncResult(new Error('Something went wrong'), [], null) + ) + this.isLoading$.next(false) + } + + async simulateLoad(): Promise { + const result$ = of( + createSuccessAsyncResult([{ id: 1, name: 'Loaded' }]) + ).pipe( + delay(1500), + map((r) => r) + ) + await executeAsyncOperation( + result$, + { + success: (r) => this.asyncResult.set(r), + error: (r) => this.asyncResult.set(r) + }, + this.isLoading$, + true, + false + ) + } + + checkIsAsyncResult(): void { + const current = this.asyncResult() + const result = isAsyncResult(current) ? `Yes, status: ${current.status}` : 'No' + this.isAsyncResultGuardResult.set(result) + } +} diff --git a/src/app/components-dashboard-demo/components-dashboard-demo.component.ts b/src/app/components-dashboard-demo/components-dashboard-demo.component.ts index f3df7788..4560abf3 100644 --- a/src/app/components-dashboard-demo/components-dashboard-demo.component.ts +++ b/src/app/components-dashboard-demo/components-dashboard-demo.component.ts @@ -4,6 +4,7 @@ import { MatProgressSpinner } from '@angular/material/progress-spinner' import { Router } from '@angular/router' import { DashboardItem, DashboardItemAction, DashboardItemsTableComponent } from '@ppwcode/ng-common-components' import { getFullRoutePath } from '@ppwcode/ng-router' +import type { RouteMapRoute } from '@ppwcode/ng-router' import { ROUTE_MAP } from '../app.routes' @Component({ @@ -23,7 +24,16 @@ export default class ComponentsDashboardDemoComponent { this.#getConfirmationDemoItem(), this.#getExpandableCardDemoItem(), this.#getTableDemoItem(), - this.#getMessageBarDemoItem() + this.#getMessageBarDemoItem(), + this.#getAsyncResultDemoItem(), + this.#getFileDownloadDemoItem(), + this.#getDraggableDialogDemoItem(), + this.#getFormsDemoItem(), + this.#getRouteMapDemoItem(), + this.#getSignalStoreDemoItem(), + this.#getUtilsDemoItem(), + this.#getSubscriptionHandlingDemoItem(), + this.#getLocalStorageDemoItem() ]) #getConfirmationDemoItem(): DashboardItem { @@ -109,4 +119,59 @@ export default class ComponentsDashboardDemoComponent { #openMessageBarDemo(): void { this.#router.navigateByUrl(getFullRoutePath(ROUTE_MAP.components.messageBar)) } + + #getAsyncResultDemoItem(): DashboardItem { + return this.#item('navigation.async_result', 'dashboard.async-result-description', 'fa-solid fa-spinner', ROUTE_MAP.components.asyncResult) + } + + #getFileDownloadDemoItem(): DashboardItem { + return this.#item('navigation.file_download', 'dashboard.file-download-description', 'fa-solid fa-download', ROUTE_MAP.components.fileDownload) + } + + #getDraggableDialogDemoItem(): DashboardItem { + return this.#item('navigation.draggable_dialog', 'dashboard.draggable-dialog-description', 'fa-solid fa-arrows-up-down-left-right', ROUTE_MAP.components.draggableDialog) + } + + #getFormsDemoItem(): DashboardItem { + return this.#item('navigation.forms_demo', 'dashboard.forms-demo-description', 'fa-solid fa-pen-to-square', ROUTE_MAP.components.formsDemo) + } + + #getRouteMapDemoItem(): DashboardItem { + return this.#item('navigation.route_map', 'dashboard.route-map-description', 'fa-solid fa-route', ROUTE_MAP.components.routeMap) + } + + #getSignalStoreDemoItem(): DashboardItem { + return this.#item('navigation.signal_store', 'dashboard.signal-store-description', 'fa-solid fa-database', ROUTE_MAP.components.signalStore) + } + + #getUtilsDemoItem(): DashboardItem { + return this.#item('navigation.utils_demo', 'dashboard.utils-demo-description', 'fa-solid fa-wrench', ROUTE_MAP.components.utilsDemo) + } + + #getSubscriptionHandlingDemoItem(): DashboardItem { + return this.#item('navigation.subscription_handling', 'dashboard.subscription-handling-description', 'fa-solid fa-link-slash', ROUTE_MAP.subscriptionHandling) + } + + #getLocalStorageDemoItem(): DashboardItem { + return this.#item('navigation.local_storage', 'dashboard.local-storage-description', 'fa-solid fa-hard-drive', ROUTE_MAP.localStorage) + } + + #item( + titleKey: string, + descriptionKey: string, + iconClass: string, + route: RouteMapRoute + ): DashboardItem { + const openAction: DashboardItemAction = { + labelKey: 'button.open', + clickFn: () => this.#router.navigateByUrl(getFullRoutePath(route)) + } + return { + iconClass, + titleKey, + descriptionKey, + actions: [openAction], + defaultAction: openAction + } + } } diff --git a/src/app/components.routes.ts b/src/app/components.routes.ts index 29f1f88c..b6ab37a1 100644 --- a/src/app/components.routes.ts +++ b/src/app/components.routes.ts @@ -30,5 +30,41 @@ export const componentsRoutes: Routes = [ path: getRouteSegment(ROUTE_MAP.components.messageBar), component: MessageBarComponent, title: 'navigation.message_bar' + }, + { + path: getRouteSegment(ROUTE_MAP.components.asyncResult), + loadComponent: () => import('./async-result-demo/async-result-demo.component').then((m) => m.AsyncResultDemoComponent), + title: 'navigation.async_result' + }, + { + path: getRouteSegment(ROUTE_MAP.components.fileDownload), + loadComponent: () => import('./file-download-demo/file-download-demo.component').then((m) => m.FileDownloadDemoComponent), + title: 'navigation.file_download' + }, + { + path: getRouteSegment(ROUTE_MAP.components.draggableDialog), + loadComponent: () => + import('./draggable-dialog-demo/draggable-dialog-demo.component').then((m) => m.DraggableDialogDemoComponent), + title: 'navigation.draggable_dialog' + }, + { + path: getRouteSegment(ROUTE_MAP.components.formsDemo), + loadComponent: () => import('./forms-demo/forms-demo.component').then((m) => m.FormsDemoComponent), + title: 'navigation.forms_demo' + }, + { + path: getRouteSegment(ROUTE_MAP.components.routeMap), + loadComponent: () => import('./route-map-demo/route-map-demo.component').then((m) => m.RouteMapDemoComponent), + title: 'navigation.route_map' + }, + { + path: getRouteSegment(ROUTE_MAP.components.signalStore), + loadComponent: () => import('./signal-store-demo/signal-store-demo.component').then((m) => m.SignalStoreDemoComponent), + title: 'navigation.signal_store' + }, + { + path: getRouteSegment(ROUTE_MAP.components.utilsDemo), + loadComponent: () => import('./utils-demo/utils-demo.component').then((m) => m.UtilsDemoComponent), + title: 'navigation.utils_demo' } ] diff --git a/src/app/dashboard-item-demo/dashboard-item-demo.component.html b/src/app/dashboard-item-demo/dashboard-item-demo.component.html index c6a1e8e3..e3964730 100644 --- a/src/app/dashboard-item-demo/dashboard-item-demo.component.html +++ b/src/app/dashboard-item-demo/dashboard-item-demo.component.html @@ -1,3 +1,6 @@ +

+ This demo showcases the ppwcode Angular SDK: ng-async, ng-common, ng-common-components, ng-dialogs, ng-forms, ng-router, ng-state-management, ng-utils, ng-wireframe, and ng-unit-testing. Use the sidebar or the cards below to open each demo. +

+

Draggable Dialog

+ +

This dialog can be dragged by the title bar. Uses ppwDraggableDialog and cdkDragHandle from ng-dialogs.

+
+ + + + + `, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DraggableDialogContentComponent {} diff --git a/src/app/draggable-dialog-demo/draggable-dialog-demo.component.html b/src/app/draggable-dialog-demo/draggable-dialog-demo.component.html new file mode 100644 index 00000000..1794ddc8 --- /dev/null +++ b/src/app/draggable-dialog-demo/draggable-dialog-demo.component.html @@ -0,0 +1,4 @@ +
+

Demonstrates the DraggableDialogDirective from ng-dialogs. Click the button to open a dialog that can be dragged by its title bar.

+ +
diff --git a/src/app/draggable-dialog-demo/draggable-dialog-demo.component.scss b/src/app/draggable-dialog-demo/draggable-dialog-demo.component.scss new file mode 100644 index 00000000..7f26ddcb --- /dev/null +++ b/src/app/draggable-dialog-demo/draggable-dialog-demo.component.scss @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/src/app/draggable-dialog-demo/draggable-dialog-demo.component.ts b/src/app/draggable-dialog-demo/draggable-dialog-demo.component.ts new file mode 100644 index 00000000..72e45027 --- /dev/null +++ b/src/app/draggable-dialog-demo/draggable-dialog-demo.component.ts @@ -0,0 +1,22 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core' +import { MatButtonModule } from '@angular/material/button' +import { MatDialog } from '@angular/material/dialog' +import { DraggableDialogContentComponent } from './draggable-dialog-content.component' + +@Component({ + selector: 'ppw-draggable-dialog-demo', + standalone: true, + imports: [MatButtonModule], + templateUrl: './draggable-dialog-demo.component.html', + styleUrl: './draggable-dialog-demo.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DraggableDialogDemoComponent { + private readonly dialog = inject(MatDialog) + + openDraggableDialog(): void { + this.dialog.open(DraggableDialogContentComponent, { + width: '400px' + }) + } +} diff --git a/src/app/examples/http-call-tester-example.spec.ts b/src/app/examples/http-call-tester-example.spec.ts new file mode 100644 index 00000000..30786f1a --- /dev/null +++ b/src/app/examples/http-call-tester-example.spec.ts @@ -0,0 +1,45 @@ +import { HttpClient, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http' +import { provideHttpClientTesting } from '@angular/common/http/testing' +import { TestBed } from '@angular/core/testing' +import { HttpCallTester } from '@ppwcode/ng-unit-testing' + +/** + * Example spec demonstrating HttpCallTester from @ppwcode/ng-unit-testing. + * Use this as reference when testing services that perform HTTP calls. + */ +describe('HttpCallTester example', () => { + let httpClient: HttpClient + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()] + }) + httpClient = TestBed.inject(HttpClient) + }) + + it('should verify a successful GET request', () => { + const mockUsers = [{ id: 1, name: 'Alpha' }] + + HttpCallTester.expectOneCallToUrl('/api/users') + .whenSubscribingTo(httpClient.get('/api/users')) + .expectRequestTo((request) => { + expect(request.request.method).toBe('GET') + }) + .withResponse(mockUsers) + .expectStreamResultTo((result) => { + expect(result).toEqual(mockUsers) + expect(result.length).toBe(1) + }) + .verify() + }) + + it('should verify an HTTP error is handled', () => { + HttpCallTester.expectOneCallToUrl('/api/users/999') + .whenSubscribingTo(httpClient.get('/api/users/999')) + .withResponse(null, { status: 404, statusText: 'Not Found' }) + .expectErrorTo((error) => { + expect((error as { status?: number }).status).toBe(404) + }) + .verifyFailure() + }) +}) diff --git a/src/app/file-download-demo/file-download-demo.component.html b/src/app/file-download-demo/file-download-demo.component.html new file mode 100644 index 00000000..afe9ac24 --- /dev/null +++ b/src/app/file-download-demo/file-download-demo.component.html @@ -0,0 +1,11 @@ +
+

Demonstrates file download helpers from ng-async: saveDownloadedFile (fixed filename) and saveFileDownload (filename from response / FileDownload).

+
+ + +
+
diff --git a/src/app/file-download-demo/file-download-demo.component.scss b/src/app/file-download-demo/file-download-demo.component.scss new file mode 100644 index 00000000..7f26ddcb --- /dev/null +++ b/src/app/file-download-demo/file-download-demo.component.scss @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/src/app/file-download-demo/file-download-demo.component.ts b/src/app/file-download-demo/file-download-demo.component.ts new file mode 100644 index 00000000..cc2d4f2b --- /dev/null +++ b/src/app/file-download-demo/file-download-demo.component.ts @@ -0,0 +1,34 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core' +import { MatButtonModule } from '@angular/material/button' +import { + createSuccessAsyncResult, + saveDownloadedFile, + saveFileDownload +} from '@ppwcode/ng-async' + +@Component({ + selector: 'ppw-file-download-demo', + standalone: true, + imports: [MatButtonModule], + templateUrl: './file-download-demo.component.html', + styleUrl: './file-download-demo.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class FileDownloadDemoComponent { + downloadWithFixedName(): void { + const content = 'Demo content from ppwcode Angular SDK – saveDownloadedFile demo.' + const blob = new Blob([content], { type: 'text/plain' }) + const result = createSuccessAsyncResult(blob) + saveDownloadedFile('demo-download.txt')(result) + } + + downloadWithFileNameFromResponse(): void { + const content = 'Content with filename from response – saveFileDownload demo.' + const blob = new Blob([content], { type: 'text/plain' }) + const result = createSuccessAsyncResult({ + blob, + fileName: 'server-filename-demo.txt' + }) + saveFileDownload()(result) + } +} diff --git a/src/app/forms-demo/forms-demo.component.html b/src/app/forms-demo/forms-demo.component.html new file mode 100644 index 00000000..2fe10f6b --- /dev/null +++ b/src/app/forms-demo/forms-demo.component.html @@ -0,0 +1,27 @@ +
+

Demonstrates ng-forms: createNonNullableControl (required, notOnlySpacesValidator), createNullableControl (optional nickname). Reset restores non-nullable to initial value and nullable to null.

+
+ + Name (required, no spaces only) + + @if (form.get('name')?.invalid && form.get('name')?.touched) { + Required and cannot be only spaces + } + + + Nickname (optional, nullable) + + + + Email (required) + + @if (form.get('email')?.invalid && form.get('email')?.touched) { + Valid email required + } + +
+ + +
+
+
diff --git a/src/app/forms-demo/forms-demo.component.scss b/src/app/forms-demo/forms-demo.component.scss new file mode 100644 index 00000000..7f26ddcb --- /dev/null +++ b/src/app/forms-demo/forms-demo.component.scss @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/src/app/forms-demo/forms-demo.component.ts b/src/app/forms-demo/forms-demo.component.ts new file mode 100644 index 00000000..9352f54d --- /dev/null +++ b/src/app/forms-demo/forms-demo.component.ts @@ -0,0 +1,50 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core' +import { FormGroup, ReactiveFormsModule, Validators } from '@angular/forms' +import { MatButtonModule } from '@angular/material/button' +import { MatFormFieldModule } from '@angular/material/form-field' +import { MatInputModule } from '@angular/material/input' +import { + createNonNullableControl, + createNullableControl, + ValidationService +} from '@ppwcode/ng-forms' + +@Component({ + selector: 'ppw-forms-demo', + standalone: true, + imports: [ + ReactiveFormsModule, + MatFormFieldModule, + MatInputModule, + MatButtonModule + ], + templateUrl: './forms-demo.component.html', + styleUrl: './forms-demo.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class FormsDemoComponent { + readonly form = new FormGroup({ + name: createNonNullableControl('', [ + Validators.required, + ValidationService.notOnlySpacesValidator + ]), + nickname: createNullableControl(null), + email: createNonNullableControl('', { + validators: [Validators.required, Validators.email] + }) + }) + + submit(): void { + if (this.form.valid) { + console.log('Submitted:', this.form.getRawValue()) + } + } + + reset(): void { + this.form.reset({ + name: '', + nickname: null, + email: '' + }) + } +} diff --git a/src/app/local-storage-demo/local-storage-demo.component.html b/src/app/local-storage-demo/local-storage-demo.component.html new file mode 100644 index 00000000..512866db --- /dev/null +++ b/src/app/local-storage-demo/local-storage-demo.component.html @@ -0,0 +1,13 @@ +
+

Demonstrates LOCAL_STORAGE_TOKEN from ng-common: getItem, setItem, removeItem. Uses provideLocalStorage() from app config.

+ + Value to store + + +
+ + + +
+

Last read value: {{ lastRead() ?? '(empty)' }}

+
diff --git a/src/app/local-storage-demo/local-storage-demo.component.scss b/src/app/local-storage-demo/local-storage-demo.component.scss new file mode 100644 index 00000000..7f26ddcb --- /dev/null +++ b/src/app/local-storage-demo/local-storage-demo.component.scss @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/src/app/local-storage-demo/local-storage-demo.component.ts b/src/app/local-storage-demo/local-storage-demo.component.ts new file mode 100644 index 00000000..48b7b993 --- /dev/null +++ b/src/app/local-storage-demo/local-storage-demo.component.ts @@ -0,0 +1,44 @@ +import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core' +import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms' +import { MatButtonModule } from '@angular/material/button' +import { MatFormFieldModule } from '@angular/material/form-field' +import { MatInputModule } from '@angular/material/input' +import { LOCAL_STORAGE_TOKEN } from '@ppwcode/ng-common' + +const DEMO_KEY = 'ppw-demo-storage-key' + +@Component({ + selector: 'ppw-local-storage-demo', + standalone: true, + imports: [ + MatButtonModule, + MatFormFieldModule, + MatInputModule, + ReactiveFormsModule, + FormsModule + ], + templateUrl: './local-storage-demo.component.html', + styleUrl: './local-storage-demo.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class LocalStorageDemoComponent { + private readonly storage = inject(LOCAL_STORAGE_TOKEN) + + readonly value = new FormControl(this.storage.getItem(DEMO_KEY) ?? '', { nonNullable: true }) + readonly lastRead = signal(this.storage.getItem(DEMO_KEY)) + + save(): void { + this.storage.setItem(DEMO_KEY, this.value.value) + this.lastRead.set(this.value.value) + } + + clear(): void { + this.storage.removeItem(DEMO_KEY) + this.value.setValue('') + this.lastRead.set(null) + } + + read(): void { + this.lastRead.set(this.storage.getItem(DEMO_KEY)) + } +} diff --git a/src/app/route-map-demo/route-map-demo.component.html b/src/app/route-map-demo/route-map-demo.component.html new file mode 100644 index 00000000..272dce71 --- /dev/null +++ b/src/app/route-map-demo/route-map-demo.component.html @@ -0,0 +1,8 @@ +
+

Type-safe routing with ng-router: getFullRoutePath(route) and ppwRouteMapRoute pipe for routerLink. Links below use the route map.

+ +
diff --git a/src/app/route-map-demo/route-map-demo.component.scss b/src/app/route-map-demo/route-map-demo.component.scss new file mode 100644 index 00000000..3b16ab16 --- /dev/null +++ b/src/app/route-map-demo/route-map-demo.component.scss @@ -0,0 +1,8 @@ +:host { + display: block; +} + +.route-links { + list-style: disc; + padding-left: 1.5rem; +} diff --git a/src/app/route-map-demo/route-map-demo.component.ts b/src/app/route-map-demo/route-map-demo.component.ts new file mode 100644 index 00000000..714b9a73 --- /dev/null +++ b/src/app/route-map-demo/route-map-demo.component.ts @@ -0,0 +1,18 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core' +import { RouterLink } from '@angular/router' +import { getFullRoutePath } from '@ppwcode/ng-router' +import { RouteMapRoutePipe } from '@ppwcode/ng-router' +import { ROUTE_MAP } from '../app.routes' + +@Component({ + selector: 'ppw-route-map-demo', + standalone: true, + imports: [RouterLink, RouteMapRoutePipe], + templateUrl: './route-map-demo.component.html', + styleUrl: './route-map-demo.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class RouteMapDemoComponent { + readonly ROUTE_MAP = ROUTE_MAP + readonly getFullRoutePath = getFullRoutePath +} diff --git a/src/app/signal-store-demo/demo-item.store.ts b/src/app/signal-store-demo/demo-item.store.ts new file mode 100644 index 00000000..e26f25c5 --- /dev/null +++ b/src/app/signal-store-demo/demo-item.store.ts @@ -0,0 +1,50 @@ +import { Injectable, inject, computed } from '@angular/core' +import { SignalStore } from '@ppwcode/ng-state-management' + +export interface DemoItem { + id: number + name: string +} + +interface DemoItemState extends Record { + items: DemoItem[] + isLoading: boolean + searchTerm: string +} + +@Injectable({ providedIn: 'root' }) +export class DemoItemStore extends SignalStore { + constructor() { + super() + this.initialize({ + items: [], + isLoading: false, + searchTerm: '' + }) + } + + filteredItems = this.selectMany(['items', 'searchTerm'], ({ items, searchTerm }) => + searchTerm + ? items.filter((item) => + item.name.toLowerCase().includes(searchTerm.toLowerCase()) + ) + : items + ) + + async loadItems(): Promise { + this.patch({ isLoading: true }) + await new Promise((r) => setTimeout(r, 800)) + this.patch({ + items: [ + { id: 1, name: 'Alpha' }, + { id: 2, name: 'Beta' }, + { id: 3, name: 'Gamma' } + ], + isLoading: false + }) + } + + setSearchTerm(searchTerm: string): void { + this.patch({ searchTerm }) + } +} diff --git a/src/app/signal-store-demo/signal-store-demo.component.html b/src/app/signal-store-demo/signal-store-demo.component.html new file mode 100644 index 00000000..f88e430e --- /dev/null +++ b/src/app/signal-store-demo/signal-store-demo.component.html @@ -0,0 +1,15 @@ +
+

SignalStore from ng-state-management: initialize, patch, select, selectMany. Load triggers async patch; filter uses selectMany.

+ + + Filter + + + +
    + @for (item of store.filteredItems(); track item.id) { +
  • {{ item.name }}
  • + } +
+
+
diff --git a/src/app/signal-store-demo/signal-store-demo.component.scss b/src/app/signal-store-demo/signal-store-demo.component.scss new file mode 100644 index 00000000..7f26ddcb --- /dev/null +++ b/src/app/signal-store-demo/signal-store-demo.component.scss @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/src/app/signal-store-demo/signal-store-demo.component.ts b/src/app/signal-store-demo/signal-store-demo.component.ts new file mode 100644 index 00000000..d512f5b0 --- /dev/null +++ b/src/app/signal-store-demo/signal-store-demo.component.ts @@ -0,0 +1,22 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core' +import { MatButtonModule } from '@angular/material/button' +import { MatFormFieldModule } from '@angular/material/form-field' +import { MatInputModule } from '@angular/material/input' +import { LoaderComponent } from '@ppwcode/ng-common-components' +import { DemoItemStore } from './demo-item.store' + +@Component({ + selector: 'ppw-signal-store-demo', + standalone: true, + imports: [MatButtonModule, MatFormFieldModule, MatInputModule, LoaderComponent], + templateUrl: './signal-store-demo.component.html', + styleUrl: './signal-store-demo.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class SignalStoreDemoComponent { + readonly store = inject(DemoItemStore) + + onSearchTermChange(value: string): void { + this.store.setSearchTerm(value) + } +} diff --git a/src/app/subscription-handling-demo/subscription-handling-demo.component.html b/src/app/subscription-handling-demo/subscription-handling-demo.component.html new file mode 100644 index 00000000..a94d6c7f --- /dev/null +++ b/src/app/subscription-handling-demo/subscription-handling-demo.component.html @@ -0,0 +1,4 @@ +
+

This component extends mixinHandleSubscriptions() and uses stopOnDestroy(interval(1000)) so the interval is automatically unsubscribed when the component is destroyed. Navigate away and back to see the counter reset.

+

Counter (ticks every second): {{ counter() }}

+
diff --git a/src/app/subscription-handling-demo/subscription-handling-demo.component.scss b/src/app/subscription-handling-demo/subscription-handling-demo.component.scss new file mode 100644 index 00000000..7f26ddcb --- /dev/null +++ b/src/app/subscription-handling-demo/subscription-handling-demo.component.scss @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/src/app/subscription-handling-demo/subscription-handling-demo.component.ts b/src/app/subscription-handling-demo/subscription-handling-demo.component.ts new file mode 100644 index 00000000..649291d7 --- /dev/null +++ b/src/app/subscription-handling-demo/subscription-handling-demo.component.ts @@ -0,0 +1,25 @@ +import { ChangeDetectionStrategy, Component, signal } from '@angular/core' +import { MatButtonModule } from '@angular/material/button' +import { mixinHandleSubscriptions } from '@ppwcode/ng-common' +import { interval } from 'rxjs' + +const SubscriptionHandlerBase = mixinHandleSubscriptions() + +@Component({ + selector: 'ppw-subscription-handling-demo', + standalone: true, + imports: [MatButtonModule], + templateUrl: './subscription-handling-demo.component.html', + styleUrl: './subscription-handling-demo.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class SubscriptionHandlingDemoComponent extends SubscriptionHandlerBase { + readonly counter = signal(0) + + constructor() { + super() + this.stopOnDestroy(interval(1000)).subscribe((value) => { + this.counter.set(value) + }) + } +} diff --git a/src/app/utils-demo/utils-demo.component.html b/src/app/utils-demo/utils-demo.component.html new file mode 100644 index 00000000..9bcada59 --- /dev/null +++ b/src/app/utils-demo/utils-demo.component.html @@ -0,0 +1,36 @@ +
+

Assertion utilities from ng-utils: natural, noDuplicates, notUndefined, notNull.

+
+

natural(n?)

+ + @if (naturalResults(); as r) { +
    + @for (entry of r | keyvalue; track entry.key) { +
  • {{ entry.key }} = {{ entry.value }}
  • + } +
+ } +
+
+

noDuplicates(arr)

+ + @if (noDuplicatesResults(); as r) { +
    + @for (entry of r | keyvalue; track entry.key) { +
  • {{ entry.key }} = {{ entry.value }}
  • + } +
+ } +
+
+

notUndefined / notNull

+ + + @if (notUndefinedResult()) { +

{{ notUndefinedResult() }}

+ } + @if (notNullResult()) { +

{{ notNullResult() }}

+ } +
+
diff --git a/src/app/utils-demo/utils-demo.component.scss b/src/app/utils-demo/utils-demo.component.scss new file mode 100644 index 00000000..7f26ddcb --- /dev/null +++ b/src/app/utils-demo/utils-demo.component.scss @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/src/app/utils-demo/utils-demo.component.ts b/src/app/utils-demo/utils-demo.component.ts new file mode 100644 index 00000000..ca9e960c --- /dev/null +++ b/src/app/utils-demo/utils-demo.component.ts @@ -0,0 +1,57 @@ +import { KeyValuePipe } from '@angular/common' +import { ChangeDetectionStrategy, Component, signal } from '@angular/core' +import { MatButtonModule } from '@angular/material/button' +import { natural, noDuplicates, notNull, notUndefined } from '@ppwcode/ng-utils' + +@Component({ + selector: 'ppw-utils-demo', + standalone: true, + imports: [MatButtonModule, KeyValuePipe], + templateUrl: './utils-demo.component.html', + styleUrl: './utils-demo.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class UtilsDemoComponent { + readonly naturalResults = signal>({}) + readonly noDuplicatesResults = signal>({}) + readonly notUndefinedResult = signal('') + readonly notNullResult = signal('') + + runNatural(): void { + this.naturalResults.set({ + 'natural(0)': natural(0), + 'natural(5)': natural(5), + 'natural(-1)': natural(-1), + 'natural(1.5)': natural(1.5), + 'natural(undefined)': natural(undefined) + }) + } + + runNoDuplicates(): void { + this.noDuplicatesResults.set({ + 'noDuplicates([1,2,3])': noDuplicates([1, 2, 3]), + 'noDuplicates([1,2,2,3])': noDuplicates([1, 2, 2, 3]), + "noDuplicates(['a','b'])": noDuplicates(['a', 'b']) + }) + } + + runNotUndefined(): void { + const val: string | undefined = 'hello' + try { + const result = notUndefined(val) + this.notUndefinedResult.set(`notUndefined('hello') = ${result}`) + } catch (e) { + this.notUndefinedResult.set(`Threw: ${(e as Error).message}`) + } + } + + runNotNull(): void { + const val: string | null = 'world' + try { + const result = notNull(val) + this.notNullResult.set(`notNull('world') = ${result}`) + } catch (e) { + this.notNullResult.set(`Threw: ${(e as Error).message}`) + } + } +} diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 216c46f4..69517532 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -12,7 +12,16 @@ "confirmation-dialog-description": "A demo page showcasing the confirm dialog component", "expandable-card-description": "A demo page that shows all possibilities with the expandable card component", "global-error-handler-description": "A demo page that shows all possibilities with the global error handler", - "in-memory-logging-description": "A demo page that shows all possibilities with the in-memory logging provider" + "in-memory-logging-description": "A demo page that shows all possibilities with the in-memory logging provider", + "async-result-description": "AsyncResult states and factories from ng-async", + "file-download-description": "File download helpers saveDownloadedFile and saveFileDownload", + "draggable-dialog-description": "Draggable Material dialog with ppwDraggableDialog", + "forms-demo-description": "Form control generators createNonNullableControl and createNullableControl", + "route-map-description": "Type-safe routing with getRouteUrl and RouteMapRoutePipe", + "signal-store-description": "Signal-based state store with select and selectMany", + "utils-demo-description": "Assertion utilities notUndefined, notNull, natural, noDuplicates", + "subscription-handling-description": "mixinHandleSubscriptions and stopOnDestroy", + "local-storage-description": "LOCAL_STORAGE_TOKEN getItem/setItem/removeItem" }, "global-error-dialog": { "copy-all-errors": "Copy All Errors", @@ -35,7 +44,16 @@ "global_error_handler": "Global Error Handler", "in_memory_logging": "In Memory Logging", "message_bar": "Message Bar", - "peopleware_website": "PeopleWare Website" + "peopleware_website": "PeopleWare Website", + "async_result": "Async Result", + "file_download": "File Download", + "draggable_dialog": "Draggable Dialog", + "forms_demo": "Form Controls", + "route_map": "Route Map", + "signal_store": "Signal Store", + "utils_demo": "Assertion Utils", + "subscription_handling": "Subscription Handling", + "local_storage": "Local Storage" }, "table": { "age": "Age", diff --git a/src/assets/i18n/nl.json b/src/assets/i18n/nl.json index cad54242..d4b3a9e7 100644 --- a/src/assets/i18n/nl.json +++ b/src/assets/i18n/nl.json @@ -12,7 +12,16 @@ "confirmation-dialog-description": "Een demo pagina die de confirmation dialog component laat zien", "expandable-card-description": "Een demo pagina die alle mogelijkheden met de expandable card component laat zien", "global-error-handler-description": "Een demo pagina die alle mogelijkheden van de global error handler laat zien", - "in-memory-logging-description": "Een demo pagina die alle mogelijkheden van de in-memory logging provider laat zien" + "in-memory-logging-description": "Een demo pagina die alle mogelijkheden van de in-memory logging provider laat zien", + "async-result-description": "AsyncResult staten en factories van ng-async", + "file-download-description": "File download helpers saveDownloadedFile en saveFileDownload", + "draggable-dialog-description": "Versleepbare Material dialog met ppwDraggableDialog", + "forms-demo-description": "Form control generators createNonNullableControl en createNullableControl", + "route-map-description": "Type-veilige routing met getRouteUrl en RouteMapRoutePipe", + "signal-store-description": "Signal-based state store met select en selectMany", + "utils-demo-description": "Assertion utilities notUndefined, notNull, natural, noDuplicates", + "subscription-handling-description": "mixinHandleSubscriptions en stopOnDestroy", + "local-storage-description": "LOCAL_STORAGE_TOKEN getItem/setItem/removeItem" }, "global-error-dialog": { "copy-all-errors": "Kopieer alle fouten", @@ -35,7 +44,16 @@ "global_error_handler": "Global Error Handler", "in_memory_logging": "In Memory Logging", "message_bar": "Message Bar", - "peopleware_website": "PeopleWare Website" + "peopleware_website": "PeopleWare Website", + "async_result": "Async Result", + "file_download": "File Download", + "draggable_dialog": "Draggable Dialog", + "forms_demo": "Form Controls", + "route_map": "Route Map", + "signal_store": "Signal Store", + "utils_demo": "Assertion Utils", + "subscription_handling": "Subscription Handling", + "local_storage": "Local Storage" }, "table": { "age": "Leeftijd", diff --git a/src/main.ts b/src/main.ts index f22c8fad..86e359d0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,7 +13,7 @@ import { PpwAsyncResultDefaultOptions, ppwHttpErrorExtractorWithTranslatedMessages } from '@ppwcode/ng-async' -import { provideGlobalErrorHandler } from '@ppwcode/ng-common' +import { provideGlobalErrorHandler, provideLocalStorage, provideLogger } from '@ppwcode/ng-common' import { PPW_TABLE_DEFAULT_OPTIONS } from '@ppwcode/ng-common-components' import { provideBreadcrumbOptions, providePaginationOptions, TranslatedPageTitleStrategy } from '@ppwcode/ng-router' import { AppComponent } from './app/app.component' @@ -51,6 +51,8 @@ bootstrapApplication(AppComponent, { useValue: { emptyResultComponent: EmptyAsyncResultComponent } as PpwAsyncResultDefaultOptions }, provideRouter(routes, withViewTransitions()), + provideLogger({ prefix: '[PPW Demo]', debug: true }), + provideLocalStorage(), provideGlobalErrorHandler({ errorDialogOptions: { allowIgnore: true,