| @@ -31,6 +31,9 @@ | |||
| - Java must be installed | |||
| - cd matsen-tool | |||
| - npm run generate:api | |||
| - Wenn es nicht geht: brew install java | |||
| - sudo ln -sfn /opt/homebrew/opt/openjdk/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk.jdk | |||
| - java -version | |||
| ## Module anlegen | |||
| - cd app | |||
| @@ -41,6 +41,7 @@ | |||
| "karma-coverage": "~2.2.0", | |||
| "karma-jasmine": "~5.1.0", | |||
| "karma-jasmine-html-reporter": "~2.1.0", | |||
| "openapi-typescript": "^7.0.0-next.5", | |||
| "typescript": "~5.2.2" | |||
| } | |||
| }, | |||
| @@ -2326,54 +2327,6 @@ | |||
| "node": ">=10.0.0" | |||
| } | |||
| }, | |||
| "node_modules/@esbuild/android-arm": { | |||
| "version": "0.19.5", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.5.tgz", | |||
| "integrity": "sha512-bhvbzWFF3CwMs5tbjf3ObfGqbl/17ict2/uwOSfr3wmxDE6VdS2GqY/FuzIPe0q0bdhj65zQsvqfArI9MY6+AA==", | |||
| "cpu": [ | |||
| "arm" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "android" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/@esbuild/android-arm64": { | |||
| "version": "0.19.5", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.5.tgz", | |||
| "integrity": "sha512-5d1OkoJxnYQfmC+Zd8NBFjkhyCNYwM4n9ODrycTFY6Jk1IGiZ+tjVJDDSwDt77nK+tfpGP4T50iMtVi4dEGzhQ==", | |||
| "cpu": [ | |||
| "arm64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "android" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/@esbuild/android-x64": { | |||
| "version": "0.19.5", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.5.tgz", | |||
| "integrity": "sha512-9t+28jHGL7uBdkBjL90QFxe7DVA+KGqWlHCF8ChTKyaKO//VLuoBricQCgwhOjA1/qOczsw843Fy4cbs4H3DVA==", | |||
| "cpu": [ | |||
| "x64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "android" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/@esbuild/darwin-arm64": { | |||
| "version": "0.19.5", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.5.tgz", | |||
| @@ -2390,294 +2343,6 @@ | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/@esbuild/darwin-x64": { | |||
| "version": "0.19.5", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.5.tgz", | |||
| "integrity": "sha512-Ly8cn6fGLNet19s0X4unjcniX24I0RqjPv+kurpXabZYSXGM4Pwpmf85WHJN3lAgB8GSth7s5A0r856S+4DyiA==", | |||
| "cpu": [ | |||
| "x64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "darwin" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/@esbuild/freebsd-arm64": { | |||
| "version": "0.19.5", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.5.tgz", | |||
| "integrity": "sha512-GGDNnPWTmWE+DMchq1W8Sd0mUkL+APvJg3b11klSGUDvRXh70JqLAO56tubmq1s2cgpVCSKYywEiKBfju8JztQ==", | |||
| "cpu": [ | |||
| "arm64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "freebsd" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/@esbuild/freebsd-x64": { | |||
| "version": "0.19.5", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.5.tgz", | |||
| "integrity": "sha512-1CCwDHnSSoA0HNwdfoNY0jLfJpd7ygaLAp5EHFos3VWJCRX9DMwWODf96s9TSse39Br7oOTLryRVmBoFwXbuuQ==", | |||
| "cpu": [ | |||
| "x64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "freebsd" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/@esbuild/linux-arm": { | |||
| "version": "0.19.5", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.5.tgz", | |||
| "integrity": "sha512-lrWXLY/vJBzCPC51QN0HM71uWgIEpGSjSZZADQhq7DKhPcI6NH1IdzjfHkDQws2oNpJKpR13kv7/pFHBbDQDwQ==", | |||
| "cpu": [ | |||
| "arm" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "linux" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/@esbuild/linux-arm64": { | |||
| "version": "0.19.5", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.5.tgz", | |||
| "integrity": "sha512-o3vYippBmSrjjQUCEEiTZ2l+4yC0pVJD/Dl57WfPwwlvFkrxoSO7rmBZFii6kQB3Wrn/6GwJUPLU5t52eq2meA==", | |||
| "cpu": [ | |||
| "arm64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "linux" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/@esbuild/linux-ia32": { | |||
| "version": "0.19.5", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.5.tgz", | |||
| "integrity": "sha512-MkjHXS03AXAkNp1KKkhSKPOCYztRtK+KXDNkBa6P78F8Bw0ynknCSClO/ztGszILZtyO/lVKpa7MolbBZ6oJtQ==", | |||
| "cpu": [ | |||
| "ia32" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "linux" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/@esbuild/linux-loong64": { | |||
| "version": "0.19.5", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.5.tgz", | |||
| "integrity": "sha512-42GwZMm5oYOD/JHqHska3Jg0r+XFb/fdZRX+WjADm3nLWLcIsN27YKtqxzQmGNJgu0AyXg4HtcSK9HuOk3v1Dw==", | |||
| "cpu": [ | |||
| "loong64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "linux" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/@esbuild/linux-mips64el": { | |||
| "version": "0.19.5", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.5.tgz", | |||
| "integrity": "sha512-kcjndCSMitUuPJobWCnwQ9lLjiLZUR3QLQmlgaBfMX23UEa7ZOrtufnRds+6WZtIS9HdTXqND4yH8NLoVVIkcg==", | |||
| "cpu": [ | |||
| "mips64el" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "linux" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/@esbuild/linux-ppc64": { | |||
| "version": "0.19.5", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.5.tgz", | |||
| "integrity": "sha512-yJAxJfHVm0ZbsiljbtFFP1BQKLc8kUF6+17tjQ78QjqjAQDnhULWiTA6u0FCDmYT1oOKS9PzZ2z0aBI+Mcyj7Q==", | |||
| "cpu": [ | |||
| "ppc64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "linux" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/@esbuild/linux-riscv64": { | |||
| "version": "0.19.5", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.5.tgz", | |||
| "integrity": "sha512-5u8cIR/t3gaD6ad3wNt1MNRstAZO+aNyBxu2We8X31bA8XUNyamTVQwLDA1SLoPCUehNCymhBhK3Qim1433Zag==", | |||
| "cpu": [ | |||
| "riscv64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "linux" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/@esbuild/linux-s390x": { | |||
| "version": "0.19.5", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.5.tgz", | |||
| "integrity": "sha512-Z6JrMyEw/EmZBD/OFEFpb+gao9xJ59ATsoTNlj39jVBbXqoZm4Xntu6wVmGPB/OATi1uk/DB+yeDPv2E8PqZGw==", | |||
| "cpu": [ | |||
| "s390x" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "linux" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/@esbuild/linux-x64": { | |||
| "version": "0.19.5", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.5.tgz", | |||
| "integrity": "sha512-psagl+2RlK1z8zWZOmVdImisMtrUxvwereIdyJTmtmHahJTKb64pAcqoPlx6CewPdvGvUKe2Jw+0Z/0qhSbG1A==", | |||
| "cpu": [ | |||
| "x64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "linux" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/@esbuild/netbsd-x64": { | |||
| "version": "0.19.5", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.5.tgz", | |||
| "integrity": "sha512-kL2l+xScnAy/E/3119OggX8SrWyBEcqAh8aOY1gr4gPvw76la2GlD4Ymf832UCVbmuWeTf2adkZDK+h0Z/fB4g==", | |||
| "cpu": [ | |||
| "x64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "netbsd" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/@esbuild/openbsd-x64": { | |||
| "version": "0.19.5", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.5.tgz", | |||
| "integrity": "sha512-sPOfhtzFufQfTBgRnE1DIJjzsXukKSvZxloZbkJDG383q0awVAq600pc1nfqBcl0ice/WN9p4qLc39WhBShRTA==", | |||
| "cpu": [ | |||
| "x64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "openbsd" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/@esbuild/sunos-x64": { | |||
| "version": "0.19.5", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.5.tgz", | |||
| "integrity": "sha512-dGZkBXaafuKLpDSjKcB0ax0FL36YXCvJNnztjKV+6CO82tTYVDSH2lifitJ29jxRMoUhgkg9a+VA/B03WK5lcg==", | |||
| "cpu": [ | |||
| "x64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "sunos" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/@esbuild/win32-arm64": { | |||
| "version": "0.19.5", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.5.tgz", | |||
| "integrity": "sha512-dWVjD9y03ilhdRQ6Xig1NWNgfLtf2o/STKTS+eZuF90fI2BhbwD6WlaiCGKptlqXlURVB5AUOxUj09LuwKGDTg==", | |||
| "cpu": [ | |||
| "arm64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "win32" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/@esbuild/win32-ia32": { | |||
| "version": "0.19.5", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.5.tgz", | |||
| "integrity": "sha512-4liggWIA4oDgUxqpZwrDhmEfAH4d0iljanDOK7AnVU89T6CzHon/ony8C5LeOdfgx60x5cnQJFZwEydVlYx4iw==", | |||
| "cpu": [ | |||
| "ia32" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "win32" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/@esbuild/win32-x64": { | |||
| "version": "0.19.5", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.5.tgz", | |||
| "integrity": "sha512-czTrygUsB/jlM8qEW5MD8bgYU2Xg14lo6kBDXW6HdxKjh8M5PzETGiSHaz9MtbXBYDloHNUAUW2tMiKW4KM9Mw==", | |||
| "cpu": [ | |||
| "x64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "win32" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/@fastify/busboy": { | |||
| "version": "2.1.0", | |||
| "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", | |||
| @@ -4384,6 +4049,95 @@ | |||
| "url": "https://opencollective.com/popperjs" | |||
| } | |||
| }, | |||
| "node_modules/@redocly/ajv": { | |||
| "version": "8.11.0", | |||
| "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.0.tgz", | |||
| "integrity": "sha512-9GWx27t7xWhDIR02PA18nzBdLcKQRgc46xNQvjFkrYk4UOmvKhJ/dawwiX0cCOeetN5LcaaiqQbVOWYK62SGHw==", | |||
| "dev": true, | |||
| "dependencies": { | |||
| "fast-deep-equal": "^3.1.1", | |||
| "json-schema-traverse": "^1.0.0", | |||
| "require-from-string": "^2.0.2", | |||
| "uri-js": "^4.2.2" | |||
| }, | |||
| "funding": { | |||
| "type": "github", | |||
| "url": "https://github.com/sponsors/epoberezkin" | |||
| } | |||
| }, | |||
| "node_modules/@redocly/openapi-core": { | |||
| "version": "1.5.0", | |||
| "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.5.0.tgz", | |||
| "integrity": "sha512-AnDLoDl1+a7mZO4+lx0KG8zH04BQx4ez6yh403PuNl9/0ygbicPPc9QG/y0/0OImChOA+knKLpJazNFjzhOAeg==", | |||
| "dev": true, | |||
| "dependencies": { | |||
| "@redocly/ajv": "^8.11.0", | |||
| "@types/node": "^14.11.8", | |||
| "colorette": "^1.2.0", | |||
| "js-levenshtein": "^1.1.6", | |||
| "js-yaml": "^4.1.0", | |||
| "lodash.isequal": "^4.5.0", | |||
| "minimatch": "^5.0.1", | |||
| "node-fetch": "^2.6.1", | |||
| "pluralize": "^8.0.0", | |||
| "yaml-ast-parser": "0.0.43" | |||
| }, | |||
| "engines": { | |||
| "node": ">=14.19.0", | |||
| "npm": ">=7.0.0" | |||
| } | |||
| }, | |||
| "node_modules/@redocly/openapi-core/node_modules/@types/node": { | |||
| "version": "14.18.63", | |||
| "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", | |||
| "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", | |||
| "dev": true | |||
| }, | |||
| "node_modules/@redocly/openapi-core/node_modules/argparse": { | |||
| "version": "2.0.1", | |||
| "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", | |||
| "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", | |||
| "dev": true | |||
| }, | |||
| "node_modules/@redocly/openapi-core/node_modules/brace-expansion": { | |||
| "version": "2.0.1", | |||
| "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", | |||
| "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", | |||
| "dev": true, | |||
| "dependencies": { | |||
| "balanced-match": "^1.0.0" | |||
| } | |||
| }, | |||
| "node_modules/@redocly/openapi-core/node_modules/colorette": { | |||
| "version": "1.4.0", | |||
| "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", | |||
| "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", | |||
| "dev": true | |||
| }, | |||
| "node_modules/@redocly/openapi-core/node_modules/js-yaml": { | |||
| "version": "4.1.0", | |||
| "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", | |||
| "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", | |||
| "dev": true, | |||
| "dependencies": { | |||
| "argparse": "^2.0.1" | |||
| }, | |||
| "bin": { | |||
| "js-yaml": "bin/js-yaml.js" | |||
| } | |||
| }, | |||
| "node_modules/@redocly/openapi-core/node_modules/minimatch": { | |||
| "version": "5.1.6", | |||
| "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", | |||
| "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", | |||
| "dev": true, | |||
| "dependencies": { | |||
| "brace-expansion": "^2.0.1" | |||
| }, | |||
| "engines": { | |||
| "node": ">=10" | |||
| } | |||
| }, | |||
| "node_modules/@schematics/angular": { | |||
| "version": "17.0.6", | |||
| "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.0.6.tgz", | |||
| @@ -9030,6 +8784,15 @@ | |||
| "jiti": "bin/jiti.js" | |||
| } | |||
| }, | |||
| "node_modules/js-levenshtein": { | |||
| "version": "1.1.6", | |||
| "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", | |||
| "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", | |||
| "dev": true, | |||
| "engines": { | |||
| "node": ">=0.10.0" | |||
| } | |||
| }, | |||
| "node_modules/js-tokens": { | |||
| "version": "4.0.0", | |||
| "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", | |||
| @@ -9730,6 +9493,12 @@ | |||
| "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", | |||
| "dev": true | |||
| }, | |||
| "node_modules/lodash.isequal": { | |||
| "version": "4.5.0", | |||
| "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", | |||
| "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", | |||
| "dev": true | |||
| }, | |||
| "node_modules/lodash.isfinite": { | |||
| "version": "3.3.2", | |||
| "resolved": "https://registry.npmjs.org/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz", | |||
| @@ -10778,7 +10547,48 @@ | |||
| "node": ">=12" | |||
| }, | |||
| "funding": { | |||
| "url": "https://github.com/sponsors/sindresorhus" | |||
| "url": "https://github.com/sponsors/sindresorhus" | |||
| } | |||
| }, | |||
| "node_modules/openapi-typescript": { | |||
| "version": "7.0.0-next.5", | |||
| "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.0.0-next.5.tgz", | |||
| "integrity": "sha512-zqEDw/FZkT0ndOCd8EybkDVwEYgaOh+ryWm6OCON70DmY9YqUnNSIVyRFVjN8hesa0bxOs9QOMzXAasczNdHbQ==", | |||
| "dev": true, | |||
| "dependencies": { | |||
| "@redocly/openapi-core": "^1.4.1", | |||
| "ansi-colors": "^4.1.3", | |||
| "supports-color": "^9.4.0", | |||
| "typescript": "^5.3.2", | |||
| "yargs-parser": "^21.1.1" | |||
| }, | |||
| "bin": { | |||
| "openapi-typescript": "bin/cli.js" | |||
| } | |||
| }, | |||
| "node_modules/openapi-typescript/node_modules/supports-color": { | |||
| "version": "9.4.0", | |||
| "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", | |||
| "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", | |||
| "dev": true, | |||
| "engines": { | |||
| "node": ">=12" | |||
| }, | |||
| "funding": { | |||
| "url": "https://github.com/chalk/supports-color?sponsor=1" | |||
| } | |||
| }, | |||
| "node_modules/openapi-typescript/node_modules/typescript": { | |||
| "version": "5.3.3", | |||
| "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", | |||
| "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", | |||
| "dev": true, | |||
| "bin": { | |||
| "tsc": "bin/tsc", | |||
| "tsserver": "bin/tsserver" | |||
| }, | |||
| "engines": { | |||
| "node": ">=14.17" | |||
| } | |||
| }, | |||
| "node_modules/openurl": { | |||
| @@ -11316,6 +11126,15 @@ | |||
| "node": "^12.20.0 || ^14.13.1 || >=16.0.0" | |||
| } | |||
| }, | |||
| "node_modules/pluralize": { | |||
| "version": "8.0.0", | |||
| "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", | |||
| "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", | |||
| "dev": true, | |||
| "engines": { | |||
| "node": ">=4" | |||
| } | |||
| }, | |||
| "node_modules/portscanner": { | |||
| "version": "2.2.0", | |||
| "resolved": "https://registry.npmjs.org/portscanner/-/portscanner-2.2.0.tgz", | |||
| @@ -13577,54 +13396,6 @@ | |||
| } | |||
| } | |||
| }, | |||
| "node_modules/vite/node_modules/@esbuild/android-arm": { | |||
| "version": "0.18.20", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", | |||
| "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", | |||
| "cpu": [ | |||
| "arm" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "android" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/vite/node_modules/@esbuild/android-arm64": { | |||
| "version": "0.18.20", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", | |||
| "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", | |||
| "cpu": [ | |||
| "arm64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "android" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/vite/node_modules/@esbuild/android-x64": { | |||
| "version": "0.18.20", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", | |||
| "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", | |||
| "cpu": [ | |||
| "x64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "android" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/vite/node_modules/@esbuild/darwin-arm64": { | |||
| "version": "0.18.20", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", | |||
| @@ -13641,294 +13412,6 @@ | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/vite/node_modules/@esbuild/darwin-x64": { | |||
| "version": "0.18.20", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", | |||
| "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", | |||
| "cpu": [ | |||
| "x64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "darwin" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { | |||
| "version": "0.18.20", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", | |||
| "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", | |||
| "cpu": [ | |||
| "arm64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "freebsd" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/vite/node_modules/@esbuild/freebsd-x64": { | |||
| "version": "0.18.20", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", | |||
| "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", | |||
| "cpu": [ | |||
| "x64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "freebsd" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/vite/node_modules/@esbuild/linux-arm": { | |||
| "version": "0.18.20", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", | |||
| "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", | |||
| "cpu": [ | |||
| "arm" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "linux" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/vite/node_modules/@esbuild/linux-arm64": { | |||
| "version": "0.18.20", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", | |||
| "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", | |||
| "cpu": [ | |||
| "arm64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "linux" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/vite/node_modules/@esbuild/linux-ia32": { | |||
| "version": "0.18.20", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", | |||
| "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", | |||
| "cpu": [ | |||
| "ia32" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "linux" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/vite/node_modules/@esbuild/linux-loong64": { | |||
| "version": "0.18.20", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", | |||
| "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", | |||
| "cpu": [ | |||
| "loong64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "linux" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/vite/node_modules/@esbuild/linux-mips64el": { | |||
| "version": "0.18.20", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", | |||
| "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", | |||
| "cpu": [ | |||
| "mips64el" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "linux" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/vite/node_modules/@esbuild/linux-ppc64": { | |||
| "version": "0.18.20", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", | |||
| "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", | |||
| "cpu": [ | |||
| "ppc64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "linux" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/vite/node_modules/@esbuild/linux-riscv64": { | |||
| "version": "0.18.20", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", | |||
| "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", | |||
| "cpu": [ | |||
| "riscv64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "linux" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/vite/node_modules/@esbuild/linux-s390x": { | |||
| "version": "0.18.20", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", | |||
| "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", | |||
| "cpu": [ | |||
| "s390x" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "linux" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/vite/node_modules/@esbuild/linux-x64": { | |||
| "version": "0.18.20", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", | |||
| "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", | |||
| "cpu": [ | |||
| "x64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "linux" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/vite/node_modules/@esbuild/netbsd-x64": { | |||
| "version": "0.18.20", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", | |||
| "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", | |||
| "cpu": [ | |||
| "x64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "netbsd" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/vite/node_modules/@esbuild/openbsd-x64": { | |||
| "version": "0.18.20", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", | |||
| "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", | |||
| "cpu": [ | |||
| "x64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "openbsd" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/vite/node_modules/@esbuild/sunos-x64": { | |||
| "version": "0.18.20", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", | |||
| "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", | |||
| "cpu": [ | |||
| "x64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "sunos" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/vite/node_modules/@esbuild/win32-arm64": { | |||
| "version": "0.18.20", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", | |||
| "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", | |||
| "cpu": [ | |||
| "arm64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "win32" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/vite/node_modules/@esbuild/win32-ia32": { | |||
| "version": "0.18.20", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", | |||
| "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", | |||
| "cpu": [ | |||
| "ia32" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "win32" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/vite/node_modules/@esbuild/win32-x64": { | |||
| "version": "0.18.20", | |||
| "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", | |||
| "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", | |||
| "cpu": [ | |||
| "x64" | |||
| ], | |||
| "dev": true, | |||
| "optional": true, | |||
| "os": [ | |||
| "win32" | |||
| ], | |||
| "engines": { | |||
| "node": ">=12" | |||
| } | |||
| }, | |||
| "node_modules/vite/node_modules/esbuild": { | |||
| "version": "0.18.20", | |||
| "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", | |||
| @@ -14509,6 +13992,12 @@ | |||
| "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", | |||
| "dev": true | |||
| }, | |||
| "node_modules/yaml-ast-parser": { | |||
| "version": "0.0.43", | |||
| "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", | |||
| "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", | |||
| "dev": true | |||
| }, | |||
| "node_modules/yargs": { | |||
| "version": "17.7.2", | |||
| "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", | |||
| @@ -8,7 +8,7 @@ | |||
| "watch": "ng build --watch --configuration development", | |||
| "test": "ng test", | |||
| "serve:ssr:matsen-tool": "node dist/matsen-tool/server/server.mjs", | |||
| "generate:api": "openapi-generator-cli generate -i ./openapi.yaml -g typescript-angular -o src/app/core/api/v1 -p=removeOperationIdPrefix=true" | |||
| "generate:api": "openapi-generator-cli generate -i ./openapi.yaml -g typescript-angular -o src/app/core/api/v1 -p=removeOperationIdPrefix=true --additional-properties=supportsES6=true,typescriptThreePlus=true" | |||
| }, | |||
| "private": true, | |||
| "dependencies": { | |||
| @@ -45,6 +45,7 @@ | |||
| "karma-coverage": "~2.2.0", | |||
| "karma-jasmine": "~5.1.0", | |||
| "karma-jasmine-html-reporter": "~2.1.0", | |||
| "openapi-typescript": "^7.0.0-next.5", | |||
| "typescript": "~5.2.2" | |||
| } | |||
| } | |||
| @@ -0,0 +1,4 @@ | |||
| <div *ngFor="let alert of alerts" class="{{cssClass(alert)}}"> | |||
| <span [innerHTML]="alert.message"></span> | |||
| <button class="btn-close" (click)="removeAlert(alert)"></button> | |||
| </div> | |||
| @@ -0,0 +1,96 @@ | |||
| import { Component, OnInit, OnDestroy, Input } from '@angular/core'; | |||
| import { Router, NavigationStart } from '@angular/router'; | |||
| import { Subscription } from 'rxjs'; | |||
| import { Alert, AlertType } from '@app/_models'; | |||
| import { AlertService } from '@app/_services'; | |||
| @Component({ selector: 'alert', templateUrl: 'alert.component.html' }) | |||
| export class AlertComponent implements OnInit, OnDestroy { | |||
| @Input() id = 'default-alert'; | |||
| @Input() fade = true; | |||
| alerts: Alert[] = []; | |||
| alertSubscription!: Subscription; | |||
| routeSubscription!: Subscription; | |||
| constructor(private router: Router, private alertService: AlertService) { } | |||
| ngOnInit() { | |||
| // subscribe to new alert notifications | |||
| this.alertSubscription = this.alertService.onAlert(this.id) | |||
| .subscribe(alert => { | |||
| // clear alerts when an empty alert is received | |||
| if (!alert.message) { | |||
| // filter out alerts without 'keepAfterRouteChange' flag | |||
| this.alerts = this.alerts.filter(x => x.keepAfterRouteChange); | |||
| // remove 'keepAfterRouteChange' flag on the rest | |||
| this.alerts.forEach(x => delete x.keepAfterRouteChange); | |||
| return; | |||
| } | |||
| // add alert to array | |||
| this.alerts.push(alert); | |||
| // auto close alert if required | |||
| if (alert.autoClose) { | |||
| setTimeout(() => this.removeAlert(alert), 3000); | |||
| } | |||
| }); | |||
| // clear alerts on location change | |||
| this.routeSubscription = this.router.events.subscribe(event => { | |||
| if (event instanceof NavigationStart) { | |||
| this.alertService.clear(this.id); | |||
| } | |||
| }); | |||
| } | |||
| ngOnDestroy() { | |||
| // unsubscribe to avoid memory leaks | |||
| this.alertSubscription.unsubscribe(); | |||
| this.routeSubscription.unsubscribe(); | |||
| } | |||
| removeAlert(alert: Alert) { | |||
| // check if already removed to prevent error on auto close | |||
| if (!this.alerts.includes(alert)) return; | |||
| if (this.fade) { | |||
| // fade out alert | |||
| alert.fade = true; | |||
| // remove alert after faded out | |||
| setTimeout(() => { | |||
| this.alerts = this.alerts.filter(x => x !== alert); | |||
| }, 250); | |||
| } else { | |||
| // remove alert | |||
| this.alerts = this.alerts.filter(x => x !== alert); | |||
| } | |||
| } | |||
| cssClass(alert: Alert) { | |||
| if (!alert) return; | |||
| const classes = ['alert', 'alert-dismissible', 'mt-4', 'container']; | |||
| const alertTypeClass = { | |||
| [AlertType.Success]: 'alert-success', | |||
| [AlertType.Error]: 'alert-danger', | |||
| [AlertType.Info]: 'alert-info', | |||
| [AlertType.Warning]: 'alert-warning' | |||
| } | |||
| if (alert.type !== undefined) { | |||
| classes.push(alertTypeClass[alert.type]); | |||
| } | |||
| if (alert.fade) { | |||
| classes.push('fade'); | |||
| } | |||
| return classes.join(' '); | |||
| } | |||
| } | |||
| @@ -0,0 +1 @@ | |||
| export * from './alert.component'; | |||
| @@ -0,0 +1,24 @@ | |||
| import { Injectable } from '@angular/core'; | |||
| import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; | |||
| import { AccountService } from '@app/_services'; | |||
| @Injectable({ providedIn: 'root' }) | |||
| export class AuthGuard implements CanActivate { | |||
| constructor( | |||
| private router: Router, | |||
| private accountService: AccountService | |||
| ) {} | |||
| canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { | |||
| const user = this.accountService.userValue; | |||
| if (user) { | |||
| // authorised so return true | |||
| return true; | |||
| } | |||
| // not logged in so redirect to login page with the return url | |||
| this.router.navigate(['/account/login'], { queryParams: { returnUrl: state.url }}); | |||
| return false; | |||
| } | |||
| } | |||
| @@ -0,0 +1,24 @@ | |||
| import { Injectable } from '@angular/core'; | |||
| import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; | |||
| import { Observable, throwError } from 'rxjs'; | |||
| import { catchError } from 'rxjs/operators'; | |||
| import { AccountService } from '@app/_services'; | |||
| @Injectable() | |||
| export class ErrorInterceptor implements HttpInterceptor { | |||
| constructor(private accountService: AccountService) {} | |||
| intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { | |||
| return next.handle(request).pipe(catchError(err => { | |||
| if ([401, 403].includes(err.status) && this.accountService.userValue) { | |||
| // auto logout if 401 or 403 response returned from api | |||
| this.accountService.logout(); | |||
| } | |||
| const error = err.error?.message || err.statusText; | |||
| console.error(err); | |||
| return throwError(() => error); | |||
| })) | |||
| } | |||
| } | |||
| @@ -0,0 +1,138 @@ | |||
| import { Injectable } from '@angular/core'; | |||
| import { HttpRequest, HttpResponse, HttpHandler, HttpEvent, HttpInterceptor, HTTP_INTERCEPTORS } from '@angular/common/http'; | |||
| import { Observable, of, throwError } from 'rxjs'; | |||
| import { delay, materialize, dematerialize } from 'rxjs/operators'; | |||
| // array in local storage for registered users | |||
| const usersKey = 'angular-14-registration-login-example-users'; | |||
| let users: any[] = JSON.parse(localStorage.getItem(usersKey)!) || []; | |||
| @Injectable() | |||
| export class FakeBackendInterceptor implements HttpInterceptor { | |||
| intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { | |||
| const { url, method, headers, body } = request; | |||
| return handleRoute(); | |||
| function handleRoute() { | |||
| switch (true) { | |||
| case url.endsWith('/users/authenticate') && method === 'POST': | |||
| return authenticate(); | |||
| case url.endsWith('/users/register') && method === 'POST': | |||
| return register(); | |||
| case url.endsWith('/users') && method === 'GET': | |||
| return getUsers(); | |||
| case url.match(/\/users\/\d+$/) && method === 'GET': | |||
| return getUserById(); | |||
| case url.match(/\/users\/\d+$/) && method === 'PUT': | |||
| return updateUser(); | |||
| case url.match(/\/users\/\d+$/) && method === 'DELETE': | |||
| return deleteUser(); | |||
| default: | |||
| // pass through any requests not handled above | |||
| return next.handle(request); | |||
| } | |||
| } | |||
| // route functions | |||
| function authenticate() { | |||
| const { username, password } = body; | |||
| const user = users.find(x => x.username === username && x.password === password); | |||
| if (!user) return error('Username or password is incorrect'); | |||
| return ok({ | |||
| ...basicDetails(user), | |||
| token: 'fake-jwt-token' | |||
| }) | |||
| } | |||
| function register() { | |||
| const user = body | |||
| if (users.find(x => x.username === user.username)) { | |||
| return error('Username "' + user.username + '" is already taken') | |||
| } | |||
| user.id = users.length ? Math.max(...users.map(x => x.id)) + 1 : 1; | |||
| users.push(user); | |||
| localStorage.setItem(usersKey, JSON.stringify(users)); | |||
| return ok(); | |||
| } | |||
| function getUsers() { | |||
| if (!isLoggedIn()) return unauthorized(); | |||
| return ok(users.map(x => basicDetails(x))); | |||
| } | |||
| function getUserById() { | |||
| if (!isLoggedIn()) return unauthorized(); | |||
| const user = users.find(x => x.id === idFromUrl()); | |||
| return ok(basicDetails(user)); | |||
| } | |||
| function updateUser() { | |||
| if (!isLoggedIn()) return unauthorized(); | |||
| let params = body; | |||
| let user = users.find(x => x.id === idFromUrl()); | |||
| // only update password if entered | |||
| if (!params.password) { | |||
| delete params.password; | |||
| } | |||
| // update and save user | |||
| Object.assign(user, params); | |||
| localStorage.setItem(usersKey, JSON.stringify(users)); | |||
| return ok(); | |||
| } | |||
| function deleteUser() { | |||
| if (!isLoggedIn()) return unauthorized(); | |||
| users = users.filter(x => x.id !== idFromUrl()); | |||
| localStorage.setItem(usersKey, JSON.stringify(users)); | |||
| return ok(); | |||
| } | |||
| // helper functions | |||
| function ok(body?: any) { | |||
| return of(new HttpResponse({ status: 200, body })) | |||
| .pipe(delay(500)); // delay observable to simulate server api call | |||
| } | |||
| function error(message: string) { | |||
| return throwError(() => ({ error: { message } })) | |||
| .pipe(materialize(), delay(500), dematerialize()); // call materialize and dematerialize to ensure delay even if an error is thrown (https://github.com/Reactive-Extensions/RxJS/issues/648); | |||
| } | |||
| function unauthorized() { | |||
| return throwError(() => ({ status: 401, error: { message: 'Unauthorized' } })) | |||
| .pipe(materialize(), delay(500), dematerialize()); | |||
| } | |||
| function basicDetails(user: any) { | |||
| const { id, username, firstName, lastName } = user; | |||
| return { id, username, firstName, lastName }; | |||
| } | |||
| function isLoggedIn() { | |||
| return headers.get('Authorization') === 'Bearer fake-jwt-token'; | |||
| } | |||
| function idFromUrl() { | |||
| const urlParts = url.split('/'); | |||
| return parseInt(urlParts[urlParts.length - 1]); | |||
| } | |||
| } | |||
| } | |||
| export const fakeBackendProvider = { | |||
| // use fake backend in place of Http service for backend-less development | |||
| provide: HTTP_INTERCEPTORS, | |||
| useClass: FakeBackendInterceptor, | |||
| multi: true | |||
| }; | |||
| @@ -0,0 +1,4 @@ | |||
| export * from './auth.guard'; | |||
| export * from './error.interceptor'; | |||
| export * from './jwt.interceptor'; | |||
| export * from './fake-backend'; | |||
| @@ -0,0 +1,27 @@ | |||
| import { Injectable } from '@angular/core'; | |||
| import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; | |||
| import { Observable } from 'rxjs'; | |||
| import { environment } from '@environments/environment'; | |||
| import { AccountService } from '@app/_services'; | |||
| @Injectable() | |||
| export class JwtInterceptor implements HttpInterceptor { | |||
| constructor(private accountService: AccountService) { } | |||
| intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { | |||
| // add auth header with jwt if user is logged in and request is to the api url | |||
| const user = this.accountService.userValue; | |||
| const isLoggedIn = user && user.token; | |||
| const isApiUrl = request.url.startsWith(environment.apiUrl); | |||
| if (isLoggedIn && isApiUrl) { | |||
| request = request.clone({ | |||
| setHeaders: { | |||
| Authorization: `Bearer ${user.token}` | |||
| } | |||
| }); | |||
| } | |||
| return next.handle(request); | |||
| } | |||
| } | |||
| @@ -0,0 +1,25 @@ | |||
| export class Alert { | |||
| id?: string; | |||
| type?: AlertType; | |||
| message?: string; | |||
| autoClose?: boolean; | |||
| keepAfterRouteChange?: boolean; | |||
| fade?: boolean; | |||
| constructor(init?:Partial<Alert>) { | |||
| Object.assign(this, init); | |||
| } | |||
| } | |||
| export enum AlertType { | |||
| Success, | |||
| Error, | |||
| Info, | |||
| Warning | |||
| } | |||
| export class AlertOptions { | |||
| id?: string; | |||
| autoClose?: boolean; | |||
| keepAfterRouteChange?: boolean; | |||
| } | |||
| @@ -0,0 +1,2 @@ | |||
| export * from './alert'; | |||
| export * from './user'; | |||
| @@ -0,0 +1,8 @@ | |||
| export class User { | |||
| id?: string; | |||
| username?: string; | |||
| password?: string; | |||
| firstName?: string; | |||
| lastName?: string; | |||
| token?: string; | |||
| } | |||
| @@ -0,0 +1,82 @@ | |||
| import { Injectable } from '@angular/core'; | |||
| import { Router } from '@angular/router'; | |||
| import { HttpClient } from '@angular/common/http'; | |||
| import { BehaviorSubject, Observable } from 'rxjs'; | |||
| import { map } from 'rxjs/operators'; | |||
| import { environment } from '@environments/environment'; | |||
| import { User } from '@app/_models'; | |||
| @Injectable({ providedIn: 'root' }) | |||
| export class AccountService { | |||
| private userSubject: BehaviorSubject<User | null>; | |||
| public user: Observable<User | null>; | |||
| constructor( | |||
| private router: Router, | |||
| private http: HttpClient | |||
| ) { | |||
| this.userSubject = new BehaviorSubject(JSON.parse(localStorage.getItem('user')!)); | |||
| this.user = this.userSubject.asObservable(); | |||
| } | |||
| public get userValue() { | |||
| return this.userSubject.value; | |||
| } | |||
| login(username: string, password: string) { | |||
| return this.http.post<User>(`${environment.apiUrl}/login`, { username, password }) | |||
| .pipe(map(user => { | |||
| // store user details and jwt token in local storage to keep user logged in between page refreshes | |||
| localStorage.setItem('user', JSON.stringify(user)); | |||
| this.userSubject.next(user); | |||
| return user; | |||
| })); | |||
| } | |||
| logout() { | |||
| // remove user from local storage and set current user to null | |||
| localStorage.removeItem('user'); | |||
| this.userSubject.next(null); | |||
| this.router.navigate(['/account/login']); | |||
| } | |||
| register(user: User) { | |||
| return this.http.post(`${environment.apiUrl}/users/register`, user); | |||
| } | |||
| getAll() { | |||
| return this.http.get<User[]>(`${environment.apiUrl}/users`); | |||
| } | |||
| getById(id: string) { | |||
| return this.http.get<User>(`${environment.apiUrl}/users/${id}`); | |||
| } | |||
| update(id: string, params: any) { | |||
| return this.http.put(`${environment.apiUrl}/users/${id}`, params) | |||
| .pipe(map(x => { | |||
| // update stored user if the logged in user updated their own record | |||
| if (id == this.userValue?.id) { | |||
| // update local storage | |||
| const user = { ...this.userValue, ...params }; | |||
| localStorage.setItem('user', JSON.stringify(user)); | |||
| // publish updated user to subscribers | |||
| this.userSubject.next(user); | |||
| } | |||
| return x; | |||
| })); | |||
| } | |||
| delete(id: string) { | |||
| return this.http.delete(`${environment.apiUrl}/users/${id}`) | |||
| .pipe(map(x => { | |||
| // auto logout if the logged in user deleted their own record | |||
| if (id == this.userValue?.id) { | |||
| this.logout(); | |||
| } | |||
| return x; | |||
| })); | |||
| } | |||
| } | |||
| @@ -0,0 +1,44 @@ | |||
| import { Injectable } from '@angular/core'; | |||
| import { Observable, Subject } from 'rxjs'; | |||
| import { filter } from 'rxjs/operators'; | |||
| import { Alert, AlertType, AlertOptions } from '@app/_models'; | |||
| @Injectable({ providedIn: 'root' }) | |||
| export class AlertService { | |||
| private subject = new Subject<Alert>(); | |||
| private defaultId = 'default-alert'; | |||
| // enable subscribing to alerts observable | |||
| onAlert(id = this.defaultId): Observable<Alert> { | |||
| return this.subject.asObservable().pipe(filter(x => x && x.id === id)); | |||
| } | |||
| // convenience methods | |||
| success(message: string, options?: AlertOptions) { | |||
| this.alert(new Alert({ ...options, type: AlertType.Success, message })); | |||
| } | |||
| error(message: string, options?: AlertOptions) { | |||
| this.alert(new Alert({ ...options, type: AlertType.Error, message })); | |||
| } | |||
| info(message: string, options?: AlertOptions) { | |||
| this.alert(new Alert({ ...options, type: AlertType.Info, message })); | |||
| } | |||
| warn(message: string, options?: AlertOptions) { | |||
| this.alert(new Alert({ ...options, type: AlertType.Warning, message })); | |||
| } | |||
| // main alert method | |||
| alert(alert: Alert) { | |||
| alert.id = alert.id || this.defaultId; | |||
| this.subject.next(alert); | |||
| } | |||
| // clear alerts | |||
| clear(id = this.defaultId) { | |||
| this.subject.next(new Alert({ id })); | |||
| } | |||
| } | |||
| @@ -0,0 +1,2 @@ | |||
| export * from './account.service'; | |||
| export * from './alert.service'; | |||
| @@ -0,0 +1,22 @@ | |||
| import { NgModule } from '@angular/core'; | |||
| import { Routes, RouterModule } from '@angular/router'; | |||
| import { LayoutComponent } from './layout.component'; | |||
| import { LoginComponent } from './login.component'; | |||
| import { RegisterComponent } from './register.component'; | |||
| const routes: Routes = [ | |||
| { | |||
| path: '', component: LayoutComponent, | |||
| children: [ | |||
| { path: 'login', component: LoginComponent }, | |||
| { path: 'register', component: RegisterComponent } | |||
| ] | |||
| } | |||
| ]; | |||
| @NgModule({ | |||
| imports: [RouterModule.forChild(routes)], | |||
| exports: [RouterModule] | |||
| }) | |||
| export class AccountRoutingModule { } | |||
| @@ -0,0 +1,22 @@ | |||
| import { NgModule } from '@angular/core'; | |||
| import { ReactiveFormsModule } from '@angular/forms'; | |||
| import { CommonModule } from '@angular/common'; | |||
| import { AccountRoutingModule } from './account-routing.module'; | |||
| import { LayoutComponent } from './layout.component'; | |||
| import { LoginComponent } from './login.component'; | |||
| import { RegisterComponent } from './register.component'; | |||
| @NgModule({ | |||
| imports: [ | |||
| CommonModule, | |||
| ReactiveFormsModule, | |||
| AccountRoutingModule | |||
| ], | |||
| declarations: [ | |||
| LayoutComponent, | |||
| LoginComponent, | |||
| RegisterComponent | |||
| ] | |||
| }) | |||
| export class AccountModule { } | |||
| @@ -0,0 +1,3 @@ | |||
| <div class="container col-md-6 offset-md-3 mt-5"> | |||
| <router-outlet></router-outlet> | |||
| </div> | |||
| @@ -0,0 +1,17 @@ | |||
| import { Component } from '@angular/core'; | |||
| import { Router } from '@angular/router'; | |||
| import { AccountService } from '@app/_services'; | |||
| @Component({ templateUrl: 'layout.component.html' }) | |||
| export class LayoutComponent { | |||
| constructor( | |||
| private router: Router, | |||
| private accountService: AccountService | |||
| ) { | |||
| // redirect to home if already logged in | |||
| if (this.accountService.userValue) { | |||
| this.router.navigate(['/']); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,28 @@ | |||
| <div class="card"> | |||
| <h4 class="card-header">Login</h4> | |||
| <div class="card-body"> | |||
| <form [formGroup]="form" (ngSubmit)="onSubmit()"> | |||
| <div class="mb-3"> | |||
| <label class="form-label">Username</label> | |||
| <input type="text" formControlName="username" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['username'].errors }" /> | |||
| <div *ngIf="submitted && f['username'].errors" class="invalid-feedback"> | |||
| <div *ngIf="f['username'].hasError('required')">Username is required</div> | |||
| </div> | |||
| </div> | |||
| <div class="mb-3"> | |||
| <label class="form-label">Password</label> | |||
| <input type="password" formControlName="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['password'].errors }" /> | |||
| <div *ngIf="submitted && f['password'].errors" class="invalid-feedback"> | |||
| <div *ngIf="f['password'].hasError('required')">Password is required</div> | |||
| </div> | |||
| </div> | |||
| <div> | |||
| <button [disabled]="loading" class="btn btn-primary"> | |||
| <span *ngIf="loading" class="spinner-border spinner-border-sm me-1"></span> | |||
| Login | |||
| </button> | |||
| <a routerLink="../register" class="btn btn-link">Register</a> | |||
| </div> | |||
| </form> | |||
| </div> | |||
| </div> | |||
| @@ -0,0 +1,58 @@ | |||
| import { Component, OnInit } from '@angular/core'; | |||
| import { Router, ActivatedRoute } from '@angular/router'; | |||
| import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | |||
| import { first } from 'rxjs/operators'; | |||
| import { AccountService, AlertService } from '@app/_services'; | |||
| @Component({ templateUrl: 'login.component.html' }) | |||
| export class LoginComponent implements OnInit { | |||
| form!: FormGroup; | |||
| loading = false; | |||
| submitted = false; | |||
| constructor( | |||
| private formBuilder: FormBuilder, | |||
| private route: ActivatedRoute, | |||
| private router: Router, | |||
| private accountService: AccountService, | |||
| private alertService: AlertService | |||
| ) { } | |||
| ngOnInit() { | |||
| this.form = this.formBuilder.group({ | |||
| username: ['', Validators.required], | |||
| password: ['', Validators.required] | |||
| }); | |||
| } | |||
| // convenience getter for easy access to form fields | |||
| get f() { return this.form.controls; } | |||
| onSubmit() { | |||
| this.submitted = true; | |||
| // reset alerts on submit | |||
| this.alertService.clear(); | |||
| // stop here if form is invalid | |||
| if (this.form.invalid) { | |||
| return; | |||
| } | |||
| this.loading = true; | |||
| this.accountService.login(this.f['username'].value, this.f['password'].value) | |||
| .pipe(first()) | |||
| .subscribe({ | |||
| next: () => { | |||
| // get return url from query parameters or default to home page | |||
| const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/'; | |||
| this.router.navigateByUrl(returnUrl); | |||
| }, | |||
| error: error => { | |||
| this.alertService.error(error); | |||
| this.loading = false; | |||
| } | |||
| }); | |||
| } | |||
| } | |||
| @@ -0,0 +1,43 @@ | |||
| <div class="card"> | |||
| <h4 class="card-header">Register</h4> | |||
| <div class="card-body"> | |||
| <form [formGroup]="form" (ngSubmit)="onSubmit()"> | |||
| <div class="mb-3"> | |||
| <label class="form-label">First Name</label> | |||
| <input type="text" formControlName="firstName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['firstName'].errors }" /> | |||
| <div *ngIf="submitted && f['firstName'].errors" class="invalid-feedback"> | |||
| <div *ngIf="f['firstName'].hasError('required')">First Name is required</div> | |||
| </div> | |||
| </div> | |||
| <div class="mb-3"> | |||
| <label class="form-label">Last Name</label> | |||
| <input type="text" formControlName="lastName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['lastName'].errors }" /> | |||
| <div *ngIf="submitted && f['lastName'].errors" class="invalid-feedback"> | |||
| <div *ngIf="f['lastName'].hasError('required')">Last Name is required</div> | |||
| </div> | |||
| </div> | |||
| <div class="mb-3"> | |||
| <label class="form-label">Username</label> | |||
| <input type="text" formControlName="username" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['username'].errors }" /> | |||
| <div *ngIf="submitted && f['username'].errors" class="invalid-feedback"> | |||
| <div *ngIf="f['username'].hasError('required')">Username is required</div> | |||
| </div> | |||
| </div> | |||
| <div class="mb-3"> | |||
| <label class="form-label">Password</label> | |||
| <input type="password" formControlName="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['password'].errors }" /> | |||
| <div *ngIf="submitted && f['password'].errors" class="invalid-feedback"> | |||
| <div *ngIf="f['password'].hasError('required')">Password is required</div> | |||
| <div *ngIf="f['password'].hasError('minlength')">Password must be at least 6 characters</div> | |||
| </div> | |||
| </div> | |||
| <div> | |||
| <button [disabled]="loading" class="btn btn-primary"> | |||
| <span *ngIf="loading" class="spinner-border spinner-border-sm me-1"></span> | |||
| Register | |||
| </button> | |||
| <a routerLink="../login" class="btn btn-link">Cancel</a> | |||
| </div> | |||
| </form> | |||
| </div> | |||
| </div> | |||
| @@ -0,0 +1,59 @@ | |||
| import { Component, OnInit } from '@angular/core'; | |||
| import { Router, ActivatedRoute } from '@angular/router'; | |||
| import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | |||
| import { first } from 'rxjs/operators'; | |||
| import { AccountService, AlertService } from '@app/_services'; | |||
| @Component({ templateUrl: 'register.component.html' }) | |||
| export class RegisterComponent implements OnInit { | |||
| form!: FormGroup; | |||
| loading = false; | |||
| submitted = false; | |||
| constructor( | |||
| private formBuilder: FormBuilder, | |||
| private route: ActivatedRoute, | |||
| private router: Router, | |||
| private accountService: AccountService, | |||
| private alertService: AlertService | |||
| ) { } | |||
| ngOnInit() { | |||
| this.form = this.formBuilder.group({ | |||
| firstName: ['', Validators.required], | |||
| lastName: ['', Validators.required], | |||
| username: ['', Validators.required], | |||
| password: ['', [Validators.required, Validators.minLength(6)]] | |||
| }); | |||
| } | |||
| // convenience getter for easy access to form fields | |||
| get f() { return this.form.controls; } | |||
| onSubmit() { | |||
| this.submitted = true; | |||
| // reset alerts on submit | |||
| this.alertService.clear(); | |||
| // stop here if form is invalid | |||
| if (this.form.invalid) { | |||
| return; | |||
| } | |||
| this.loading = true; | |||
| this.accountService.register(this.form.value) | |||
| .pipe(first()) | |||
| .subscribe({ | |||
| next: () => { | |||
| this.alertService.success('Registration successful', { keepAfterRouteChange: true }); | |||
| this.router.navigate(['../login'], { relativeTo: this.route }); | |||
| }, | |||
| error: error => { | |||
| this.alertService.error(error); | |||
| this.loading = false; | |||
| } | |||
| }); | |||
| } | |||
| } | |||
| @@ -1,10 +1,23 @@ | |||
| import { NgModule } from '@angular/core'; | |||
| import { RouterModule, Routes } from '@angular/router'; | |||
| import { Routes, RouterModule } from '@angular/router'; | |||
| const routes: Routes = [{ path: 'test', loadChildren: () => import('./test/test.module').then(m => m.TestModule) }]; | |||
| import { HomeComponent } from './home'; | |||
| import { AuthGuard } from './_helpers'; | |||
| const accountModule = () => import('./account/account.module').then(x => x.AccountModule); | |||
| const usersModule = () => import('./users/users.module').then(x => x.UsersModule); | |||
| const routes: Routes = [ | |||
| { path: '', component: HomeComponent, canActivate: [AuthGuard] }, | |||
| { path: 'users', loadChildren: usersModule, canActivate: [AuthGuard] }, | |||
| { path: 'account', loadChildren: accountModule }, | |||
| // otherwise redirect to home | |||
| { path: '**', redirectTo: '' } | |||
| ]; | |||
| @NgModule({ | |||
| imports: [RouterModule.forRoot(routes)], | |||
| exports: [RouterModule] | |||
| imports: [RouterModule.forRoot(routes)], | |||
| exports: [RouterModule] | |||
| }) | |||
| export class AppRoutingModule { } | |||
| export class AppRoutingModule { } | |||
| @@ -1,12 +1,24 @@ | |||
| <!-- nav --> | |||
| <nav class="navbar navbar-expand navbar-dark bg-dark px-3" *ngIf="user"> | |||
| <div class="navbar-nav"> | |||
| <a class="nav-item nav-link" routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">Home</a> | |||
| <a class="nav-item nav-link" routerLink="/users" routerLinkActive="active">Users</a> | |||
| <button class="btn btn-link nav-item nav-link" (click)="logout()">Logout</button> | |||
| </div> | |||
| </nav> | |||
| <app-login></app-login> | |||
| <ul> | |||
| <li>Posts</li> | |||
| <li *ngFor="let post of posts"> | |||
| <!-- main app container --> | |||
| <div class="app-container" [ngClass]="{ 'bg-light': user }"> | |||
| <alert></alert> | |||
| <router-outlet></router-outlet> | |||
| </div> | |||
| <h2>{{post.id}} - {{post.owner}}</h2> | |||
| <p>{{post.message}}</p> | |||
| </li> | |||
| </ul> | |||
| <router-outlet></router-outlet> | |||
| <!-- credits --> | |||
| <div class="text-center mt-4"> | |||
| <p> | |||
| <a href="https://jasonwatmore.com/post/2022/11/29/angular-14-user-registration-and-login-example-tutorial" target="_top">Angular 14 - User Registration and Login Example & Tutorial</a> | |||
| </p> | |||
| <p> | |||
| <a href="https://jasonwatmore.com" target="_top">JasonWatmore.com</a> | |||
| </p> | |||
| </div> | |||
| @@ -1,35 +1,17 @@ | |||
| import {Component, OnInit} from '@angular/core'; | |||
| import {ApiPostsGetCollection200Response, PostJsonld, PostService} from "./core/api/v1"; | |||
| import {Observable, Subscription} from "rxjs"; | |||
| import { Component } from '@angular/core'; | |||
| @Component({ | |||
| selector: 'app-root', | |||
| templateUrl: './app.component.html', | |||
| styleUrl: './app.component.scss' | |||
| }) | |||
| export class AppComponent implements OnInit{ | |||
| import { AccountService } from './_services'; | |||
| import { User } from './_models'; | |||
| title = 'matsen-tool'; | |||
| protected postSub: Subscription; | |||
| protected posts: Array<PostJsonld>; | |||
| constructor(private postService: PostService) { | |||
| this.postSub = new Subscription(); | |||
| this.posts = []; | |||
| } | |||
| ngOnInit(): void { | |||
| this.postSub = this.postService.postsGetCollection().subscribe( | |||
| data => { | |||
| console.log(data); | |||
| //myVariable['my:prop:name' as keyof MyModel] | |||
| this.posts = data["hydra:member"]; | |||
| console.log(this.posts); | |||
| } | |||
| ); | |||
| @Component({ selector: 'app-root', templateUrl: 'app.component.html' }) | |||
| export class AppComponent { | |||
| user?: User | null; | |||
| constructor(private accountService: AccountService) { | |||
| this.accountService.user.subscribe(x => this.user = x); | |||
| } | |||
| } | |||
| logout() { | |||
| this.accountService.logout(); | |||
| } | |||
| } | |||
| @@ -0,0 +1,11 @@ | |||
| import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; | |||
| import { provideServerRendering } from '@angular/platform-server'; | |||
| import { appConfig } from './app.config'; | |||
| const serverConfig: ApplicationConfig = { | |||
| providers: [ | |||
| provideServerRendering() | |||
| ] | |||
| }; | |||
| export const config = mergeApplicationConfig(appConfig, serverConfig); | |||
| @@ -0,0 +1,10 @@ | |||
| import { ApplicationConfig } from '@angular/core'; | |||
| import { provideRouter } from '@angular/router'; | |||
| import { routes } from './app.routes'; | |||
| import { provideClientHydration } from '@angular/platform-browser'; | |||
| import { provideAnimations } from '@angular/platform-browser/animations'; | |||
| export const appConfig: ApplicationConfig = { | |||
| providers: [provideRouter(routes), provideClientHydration(), provideAnimations()] | |||
| }; | |||
| @@ -1,36 +1,36 @@ | |||
| import { NgModule } from '@angular/core'; | |||
| import { BrowserModule, provideClientHydration } from '@angular/platform-browser'; | |||
| import { NgModule } from '@angular/core'; | |||
| import { BrowserModule } from '@angular/platform-browser'; | |||
| import { ReactiveFormsModule } from '@angular/forms'; | |||
| import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; | |||
| // used to create fake backend | |||
| import { fakeBackendProvider } from './_helpers'; | |||
| import { AppRoutingModule } from './app-routing.module'; | |||
| import { JwtInterceptor, ErrorInterceptor } from './_helpers'; | |||
| import { AppComponent } from './app.component'; | |||
| import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; | |||
| import {ApiModule, Configuration, ConfigurationParameters} from "./core/api/v1"; | |||
| import {HttpClientModule} from "@angular/common/http"; | |||
| import {environment} from "../environment"; | |||
| import { LoginModule } from './login/login.module'; | |||
| export function apiConfigFactory(): Configuration { | |||
| const params: ConfigurationParameters = { | |||
| basePath: environment.basePath, | |||
| }; | |||
| return new Configuration(params); | |||
| } | |||
| import { AlertComponent } from './_components'; | |||
| import { HomeComponent } from './home'; | |||
| @NgModule({ | |||
| declarations: [ | |||
| AppComponent | |||
| ], | |||
| imports: [ | |||
| BrowserModule, | |||
| ApiModule.forRoot(apiConfigFactory), | |||
| HttpClientModule, | |||
| AppRoutingModule, | |||
| BrowserAnimationsModule, | |||
| LoginModule | |||
| ], | |||
| providers: [ | |||
| provideClientHydration() | |||
| ], | |||
| bootstrap: [AppComponent] | |||
| imports: [ | |||
| BrowserModule, | |||
| ReactiveFormsModule, | |||
| HttpClientModule, | |||
| AppRoutingModule | |||
| ], | |||
| declarations: [ | |||
| AppComponent, | |||
| AlertComponent, | |||
| HomeComponent | |||
| ], | |||
| providers: [ | |||
| { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true }, | |||
| { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }, | |||
| // provider used to create fake backend | |||
| fakeBackendProvider | |||
| ], | |||
| bootstrap: [AppComponent] | |||
| }) | |||
| export class AppModule { } | |||
| export class AppModule { }; | |||
| @@ -0,0 +1,3 @@ | |||
| import { Routes } from '@angular/router'; | |||
| export const routes: Routes = []; | |||
| @@ -0,0 +1,7 @@ | |||
| <div class="p-4"> | |||
| <div class="container"> | |||
| <h1>Hi {{user?.firstName}}!</h1> | |||
| <p>You're logged in with Angular 14!!</p> | |||
| <p><a routerLink="/users">Manage Users</a></p> | |||
| </div> | |||
| </div> | |||
| @@ -0,0 +1,13 @@ | |||
| import { Component } from '@angular/core'; | |||
| import { User } from '@app/_models'; | |||
| import { AccountService } from '@app/_services'; | |||
| @Component({ templateUrl: 'home.component.html' }) | |||
| export class HomeComponent { | |||
| user: User | null; | |||
| constructor(private accountService: AccountService) { | |||
| this.user = this.accountService.userValue; | |||
| } | |||
| } | |||
| @@ -0,0 +1 @@ | |||
| export * from './home.component'; | |||
| @@ -1,18 +0,0 @@ | |||
| import { NgModule } from '@angular/core'; | |||
| import { CommonModule } from '@angular/common'; | |||
| import { LoginComponent } from './login/login.component'; | |||
| @NgModule({ | |||
| declarations: [ | |||
| LoginComponent | |||
| ], | |||
| exports: [ | |||
| LoginComponent | |||
| ], | |||
| imports: [ | |||
| CommonModule | |||
| ] | |||
| }) | |||
| export class LoginModule { } | |||
| @@ -1 +0,0 @@ | |||
| <p>login works!</p> | |||
| @@ -1,23 +0,0 @@ | |||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
| import { LoginComponent } from './login.component'; | |||
| describe('LoginComponent', () => { | |||
| let component: LoginComponent; | |||
| let fixture: ComponentFixture<LoginComponent>; | |||
| beforeEach(async () => { | |||
| await TestBed.configureTestingModule({ | |||
| declarations: [LoginComponent] | |||
| }) | |||
| .compileComponents(); | |||
| fixture = TestBed.createComponent(LoginComponent); | |||
| component = fixture.componentInstance; | |||
| fixture.detectChanges(); | |||
| }); | |||
| it('should create', () => { | |||
| expect(component).toBeTruthy(); | |||
| }); | |||
| }); | |||
| @@ -1,10 +0,0 @@ | |||
| import { Component } from '@angular/core'; | |||
| @Component({ | |||
| selector: 'app-login', | |||
| templateUrl: './login.component.html', | |||
| styleUrl: './login.component.scss' | |||
| }) | |||
| export class LoginComponent { | |||
| } | |||
| @@ -1,11 +0,0 @@ | |||
| import { NgModule } from '@angular/core'; | |||
| import { RouterModule, Routes } from '@angular/router'; | |||
| import { TestComponent } from './test.component'; | |||
| const routes: Routes = [{ path: '', component: TestComponent }]; | |||
| @NgModule({ | |||
| imports: [RouterModule.forChild(routes)], | |||
| exports: [RouterModule] | |||
| }) | |||
| export class TestRoutingModule { } | |||
| @@ -1 +0,0 @@ | |||
| <p>test works!</p> | |||
| @@ -1,23 +0,0 @@ | |||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
| import { TestComponent } from './test.component'; | |||
| describe('TestComponent', () => { | |||
| let component: TestComponent; | |||
| let fixture: ComponentFixture<TestComponent>; | |||
| beforeEach(async () => { | |||
| await TestBed.configureTestingModule({ | |||
| declarations: [TestComponent] | |||
| }) | |||
| .compileComponents(); | |||
| fixture = TestBed.createComponent(TestComponent); | |||
| component = fixture.componentInstance; | |||
| fixture.detectChanges(); | |||
| }); | |||
| it('should create', () => { | |||
| expect(component).toBeTruthy(); | |||
| }); | |||
| }); | |||
| @@ -1,10 +0,0 @@ | |||
| import { Component } from '@angular/core'; | |||
| @Component({ | |||
| selector: 'app-test', | |||
| templateUrl: './test.component.html', | |||
| styleUrl: './test.component.scss' | |||
| }) | |||
| export class TestComponent { | |||
| } | |||
| @@ -1,17 +0,0 @@ | |||
| import { NgModule } from '@angular/core'; | |||
| import { CommonModule } from '@angular/common'; | |||
| import { TestRoutingModule } from './test-routing.module'; | |||
| import { TestComponent } from './test.component'; | |||
| @NgModule({ | |||
| declarations: [ | |||
| TestComponent | |||
| ], | |||
| imports: [ | |||
| CommonModule, | |||
| TestRoutingModule | |||
| ] | |||
| }) | |||
| export class TestModule { } | |||
| @@ -0,0 +1,49 @@ | |||
| <h1>{{title}}</h1> | |||
| <form *ngIf="!loading" [formGroup]="form" (ngSubmit)="onSubmit()"> | |||
| <div class="row"> | |||
| <div class="mb-3 col"> | |||
| <label class="form-label">First Name</label> | |||
| <input type="text" formControlName="firstName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['firstName'].errors }" /> | |||
| <div *ngIf="submitted && f['firstName'].errors" class="invalid-feedback"> | |||
| <div *ngIf="f['firstName'].hasError('required')">First Name is required</div> | |||
| </div> | |||
| </div> | |||
| <div class="mb-3 col"> | |||
| <label class="form-label">Last Name</label> | |||
| <input type="text" formControlName="lastName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['lastName'].errors }" /> | |||
| <div *ngIf="submitted && f['lastName'].errors" class="invalid-feedback"> | |||
| <div *ngIf="f['lastName'].hasError('required')">Last Name is required</div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <div class="row"> | |||
| <div class="mb-3 col"> | |||
| <label class="form-label">Username</label> | |||
| <input type="text" formControlName="username" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['username'].errors }" /> | |||
| <div *ngIf="submitted && f['username'].errors" class="invalid-feedback"> | |||
| <div *ngIf="f['username'].hasError('required')">Username is required</div> | |||
| </div> | |||
| </div> | |||
| <div class="mb-3 col"> | |||
| <label class="form-label"> | |||
| Password | |||
| <em *ngIf="id">(Leave blank to keep the same password)</em> | |||
| </label> | |||
| <input type="password" formControlName="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['password'].errors }" /> | |||
| <div *ngIf="submitted && f['password'].errors" class="invalid-feedback"> | |||
| <div *ngIf="f['password'].hasError('required')">Password is required</div> | |||
| <div *ngIf="f['password'].hasError('minlength')">Password must be at least 6 characters</div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <div class="mb-3"> | |||
| <button [disabled]="submitting" class="btn btn-primary"> | |||
| <span *ngIf="submitting" class="spinner-border spinner-border-sm me-1"></span> | |||
| Save | |||
| </button> | |||
| <a routerLink="/users" class="btn btn-link">Cancel</a> | |||
| </div> | |||
| </form> | |||
| <div *ngIf="loading" class="text-center m-5"> | |||
| <span class="spinner-border spinner-border-lg align-center"></span> | |||
| </div> | |||
| @@ -0,0 +1,86 @@ | |||
| import { Component, OnInit } from '@angular/core'; | |||
| import { Router, ActivatedRoute } from '@angular/router'; | |||
| import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | |||
| import { first } from 'rxjs/operators'; | |||
| import { AccountService, AlertService } from '@app/_services'; | |||
| @Component({ templateUrl: 'add-edit.component.html' }) | |||
| export class AddEditComponent implements OnInit { | |||
| form!: FormGroup; | |||
| id?: string; | |||
| title!: string; | |||
| loading = false; | |||
| submitting = false; | |||
| submitted = false; | |||
| constructor( | |||
| private formBuilder: FormBuilder, | |||
| private route: ActivatedRoute, | |||
| private router: Router, | |||
| private accountService: AccountService, | |||
| private alertService: AlertService | |||
| ) { } | |||
| ngOnInit() { | |||
| this.id = this.route.snapshot.params['id']; | |||
| // form with validation rules | |||
| this.form = this.formBuilder.group({ | |||
| firstName: ['', Validators.required], | |||
| lastName: ['', Validators.required], | |||
| username: ['', Validators.required], | |||
| // password only required in add mode | |||
| password: ['', [Validators.minLength(6), ...(!this.id ? [Validators.required] : [])]] | |||
| }); | |||
| this.title = 'Add User'; | |||
| if (this.id) { | |||
| // edit mode | |||
| this.title = 'Edit User'; | |||
| this.loading = true; | |||
| this.accountService.getById(this.id) | |||
| .pipe(first()) | |||
| .subscribe(x => { | |||
| this.form.patchValue(x); | |||
| this.loading = false; | |||
| }); | |||
| } | |||
| } | |||
| // convenience getter for easy access to form fields | |||
| get f() { return this.form.controls; } | |||
| onSubmit() { | |||
| this.submitted = true; | |||
| // reset alerts on submit | |||
| this.alertService.clear(); | |||
| // stop here if form is invalid | |||
| if (this.form.invalid) { | |||
| return; | |||
| } | |||
| this.submitting = true; | |||
| this.saveUser() | |||
| .pipe(first()) | |||
| .subscribe({ | |||
| next: () => { | |||
| this.alertService.success('User saved', { keepAfterRouteChange: true }); | |||
| this.router.navigateByUrl('/users'); | |||
| }, | |||
| error: error => { | |||
| this.alertService.error(error); | |||
| this.submitting = false; | |||
| } | |||
| }) | |||
| } | |||
| private saveUser() { | |||
| // create or update user based on id param | |||
| return this.id | |||
| ? this.accountService.update(this.id!, this.form.value) | |||
| : this.accountService.register(this.form.value); | |||
| } | |||
| } | |||
| @@ -0,0 +1,5 @@ | |||
| <div class="p-4"> | |||
| <div class="container"> | |||
| <router-outlet></router-outlet> | |||
| </div> | |||
| </div> | |||
| @@ -0,0 +1,4 @@ | |||
| import { Component } from '@angular/core'; | |||
| @Component({ templateUrl: 'layout.component.html' }) | |||
| export class LayoutComponent { } | |||
| @@ -0,0 +1,31 @@ | |||
| <h1>Users</h1> | |||
| <a routerLink="add" class="btn btn-sm btn-success mb-2">Add User</a> | |||
| <table class="table table-striped"> | |||
| <thead> | |||
| <tr> | |||
| <th style="width: 30%">First Name</th> | |||
| <th style="width: 30%">Last Name</th> | |||
| <th style="width: 30%">Username</th> | |||
| <th style="width: 10%"></th> | |||
| </tr> | |||
| </thead> | |||
| <tbody> | |||
| <tr *ngFor="let user of users"> | |||
| <td>{{user.firstName}}</td> | |||
| <td>{{user.lastName}}</td> | |||
| <td>{{user.username}}</td> | |||
| <td style="white-space: nowrap"> | |||
| <a routerLink="edit/{{user.id}}" class="btn btn-sm btn-primary me-1">Edit</a> | |||
| <button (click)="deleteUser(user.id)" class="btn btn-sm btn-danger btn-delete-user" [disabled]="user.isDeleting"> | |||
| <span *ngIf="user.isDeleting" class="spinner-border spinner-border-sm"></span> | |||
| <span *ngIf="!user.isDeleting">Delete</span> | |||
| </button> | |||
| </td> | |||
| </tr> | |||
| <tr *ngIf="!users"> | |||
| <td colspan="4" class="text-center"> | |||
| <span class="spinner-border spinner-border-lg align-center"></span> | |||
| </td> | |||
| </tr> | |||
| </tbody> | |||
| </table> | |||
| @@ -0,0 +1,25 @@ | |||
| import { Component, OnInit } from '@angular/core'; | |||
| import { first } from 'rxjs/operators'; | |||
| import { AccountService } from '@app/_services'; | |||
| @Component({ templateUrl: 'list.component.html' }) | |||
| export class ListComponent implements OnInit { | |||
| users?: any[]; | |||
| constructor(private accountService: AccountService) {} | |||
| ngOnInit() { | |||
| this.accountService.getAll() | |||
| .pipe(first()) | |||
| .subscribe(users => this.users = users); | |||
| } | |||
| deleteUser(id: string) { | |||
| const user = this.users!.find(x => x.id === id); | |||
| user.isDeleting = true; | |||
| this.accountService.delete(id) | |||
| .pipe(first()) | |||
| .subscribe(() => this.users = this.users!.filter(x => x.id !== id)); | |||
| } | |||
| } | |||
| @@ -0,0 +1,23 @@ | |||
| import { NgModule } from '@angular/core'; | |||
| import { Routes, RouterModule } from '@angular/router'; | |||
| import { LayoutComponent } from './layout.component'; | |||
| import { ListComponent } from './list.component'; | |||
| import { AddEditComponent } from './add-edit.component'; | |||
| const routes: Routes = [ | |||
| { | |||
| path: '', component: LayoutComponent, | |||
| children: [ | |||
| { path: '', component: ListComponent }, | |||
| { path: 'add', component: AddEditComponent }, | |||
| { path: 'edit/:id', component: AddEditComponent } | |||
| ] | |||
| } | |||
| ]; | |||
| @NgModule({ | |||
| imports: [RouterModule.forChild(routes)], | |||
| exports: [RouterModule] | |||
| }) | |||
| export class UsersRoutingModule { } | |||
| @@ -0,0 +1,22 @@ | |||
| import { NgModule } from '@angular/core'; | |||
| import { ReactiveFormsModule } from '@angular/forms'; | |||
| import { CommonModule } from '@angular/common'; | |||
| import { UsersRoutingModule } from './users-routing.module'; | |||
| import { LayoutComponent } from './layout.component'; | |||
| import { ListComponent } from './list.component'; | |||
| import { AddEditComponent } from './add-edit.component'; | |||
| @NgModule({ | |||
| imports: [ | |||
| CommonModule, | |||
| ReactiveFormsModule, | |||
| UsersRoutingModule | |||
| ], | |||
| declarations: [ | |||
| LayoutComponent, | |||
| ListComponent, | |||
| AddEditComponent | |||
| ] | |||
| }) | |||
| export class UsersModule { } | |||
| @@ -1,4 +0,0 @@ | |||
| export const environment = { | |||
| production: true, | |||
| basePath: 'https://jsonplaceholder.typicode.com', | |||
| }; | |||
| @@ -1,5 +0,0 @@ | |||
| export const environment = { | |||
| production: false, | |||
| // basePath: 'https://matsen-tool-be.ddev.site:8443', | |||
| basePath: 'https://matsen-tool-be.ddev.site:8443', | |||
| }; | |||
| @@ -0,0 +1,4 @@ | |||
| export const environment = { | |||
| production: true, | |||
| apiUrl: 'http://localhost:4000' | |||
| }; | |||
| @@ -0,0 +1,17 @@ | |||
| // This file can be replaced during build by using the `fileReplacements` array. | |||
| // `ng build` replaces `environment.ts` with `environment.prod.ts`. | |||
| // The list of file replacements can be found in `angular.json`. | |||
| export const environment = { | |||
| production: false, | |||
| apiUrl: 'https://matsen-tool-be.ddev.site:8443' | |||
| }; | |||
| /* | |||
| * For easier debugging in development mode, you can import the following file | |||
| * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. | |||
| * | |||
| * This import should be commented out in production mode because it will have a negative impact | |||
| * on performance if an error is thrown. | |||
| */ | |||
| // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. | |||
| @@ -1,7 +1,12 @@ | |||
| import { enableProdMode } from '@angular/core'; | |||
| import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; | |||
| import { AppModule } from './app/app.module'; | |||
| import { environment } from './environments/environment'; | |||
| if (environment.production) { | |||
| enableProdMode(); | |||
| } | |||
| platformBrowserDynamic().bootstrapModule(AppModule) | |||
| .catch(err => console.error(err)); | |||
| @@ -2,3 +2,14 @@ | |||
| html, body { height: 100%; } | |||
| body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } | |||
| .app-container { | |||
| min-height: 320px; | |||
| overflow: hidden; | |||
| } | |||
| .btn-delete-user { | |||
| width: 40px; | |||
| text-align: center; | |||
| box-sizing: content-box; | |||
| } | |||
| @@ -2,17 +2,19 @@ | |||
| { | |||
| "compileOnSave": false, | |||
| "compilerOptions": { | |||
| "baseUrl": "./", | |||
| "outDir": "./dist/out-tsc", | |||
| "forceConsistentCasingInFileNames": true, | |||
| "strict": true, | |||
| "noImplicitOverride": true, | |||
| "noPropertyAccessFromIndexSignature": true, | |||
| "noPropertyAccessFromIndexSignature": false, | |||
| "noImplicitReturns": true, | |||
| "noFallthroughCasesInSwitch": true, | |||
| "skipLibCheck": true, | |||
| "esModuleInterop": true, | |||
| "sourceMap": true, | |||
| "declaration": false, | |||
| "downlevelIteration": true, | |||
| "experimentalDecorators": true, | |||
| "moduleResolution": "node", | |||
| "importHelpers": true, | |||
| @@ -22,7 +24,11 @@ | |||
| "lib": [ | |||
| "ES2022", | |||
| "dom" | |||
| ] | |||
| ], | |||
| "paths": { | |||
| "@app/*": ["src/app/*"], | |||
| "@environments/*": ["src/environments/*"] | |||
| } | |||
| }, | |||
| "angularCompilerOptions": { | |||
| "enableI18nLegacyMessageIdFormat": false, | |||