diff --git a/frontend/.gitignore b/frontend/.gitignore index 7c910e7d..f8440c5f 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -15,6 +15,7 @@ .classpath *.launch .settings/ +*.iml # misc /.sass-cache diff --git a/frontend/angular-cli.json b/frontend/angular-cli.json index f2242885..5219f830 100644 --- a/frontend/angular-cli.json +++ b/frontend/angular-cli.json @@ -32,7 +32,8 @@ "dev": "environments/environment.ts", "qa": "environments/environment.qa.ts", "prod": "environments/environment.prod.ts", - "token": "environments/environment.token.ts" + "token": "environments/environment.token.ts", + "local": "environments/environment.local.ts" } } ], diff --git a/frontend/local.proxy.conf.json b/frontend/local.proxy.conf.json new file mode 100644 index 00000000..5ee92f24 --- /dev/null +++ b/frontend/local.proxy.conf.json @@ -0,0 +1,8 @@ +{ + "/api/*": { + "target": "http://localhost:5000", + "secure": false, + "changeOrigin": true, + "logLevel": "debug" + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1675fde1..c47a2c2b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -100,6 +100,14 @@ "tslib": "^1.7.1" } }, + "@angular/cdk": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-5.2.5.tgz", + "integrity": "sha512-GN8m1d+VcCE9+Bgwv06Y8YJKyZ0i9ZIq2ZPBcJYt+KVgnVVRg4JkyUNxud07LNsvzOX22DquHqmIZiC4hAG7Ag==", + "requires": { + "tslib": "^1.7.1" + } + }, "@angular/cli": { "version": "1.7.4", "resolved": "http://registry.npmjs.org/@angular/cli/-/cli-1.7.4.tgz", @@ -492,6 +500,14 @@ "tslib": "^1.7.1" } }, + "@angular/material": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-5.2.5.tgz", + "integrity": "sha512-IltfBeTJWnmZehOQNQ7KoFs7MGWuZTe0g21hIitGkusVNt1cIoTD24xKH5jwztjH19c04IgiwonpurMKM6pBCQ==", + "requires": { + "tslib": "^1.7.1" + } + }, "@angular/platform-browser": { "version": "5.2.11", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-5.2.11.tgz", @@ -773,6 +789,48 @@ "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", "dev": true }, + "ansi-align": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", + "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", + "dev": true, + "requires": { + "string-width": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, "ansi-gray": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", @@ -1344,28 +1402,105 @@ "dev": true }, "body-parser": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", - "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", "dev": true, "requires": { - "bytes": "3.0.0", + "bytes": "3.1.0", "content-type": "~1.0.4", "debug": "2.6.9", - "depd": "~1.1.1", - "http-errors": "~1.6.2", - "iconv-lite": "0.4.19", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", "on-finished": "~2.3.0", - "qs": "6.5.1", - "raw-body": "2.3.2", - "type-is": "~1.6.15" + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" }, "dependencies": { + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", + "dev": true + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "dev": true, + "requires": { + "mime-db": "1.40.0" + } + }, "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } } } }, @@ -1403,6 +1538,60 @@ "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-3.4.1.tgz", "integrity": "sha512-yN5oZVmRCwe5aKwzRj6736nSmKDX7pLYwsXiCj/EYmo16hODaBiT4En5btW/jhBF/seV+XMx3aYwukYC3A49DA==" }, + "boxen": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", + "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", + "dev": true, + "requires": { + "ansi-align": "^2.0.0", + "camelcase": "^4.0.0", + "chalk": "^2.0.1", + "cli-boxes": "^1.0.0", + "string-width": "^2.0.0", + "term-size": "^1.2.0", + "widest-line": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1672,6 +1861,12 @@ "integrity": "sha512-cXKbYwpxBLd7qHyej16JazPoUacqoVuDhvR61U7Fr5vSxMUiodzcYa1rQYRYfZ5GexV03vGZHd722vNPLjPJGQ==", "dev": true }, + "capture-stack-trace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz", + "integrity": "sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==", + "dev": true + }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -1721,6 +1916,12 @@ "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=", "dev": true }, + "ci-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", + "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", + "dev": true + }, "cipher-base": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", @@ -1780,6 +1981,12 @@ } } }, + "cli-boxes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", + "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", + "dev": true + }, "cliui": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", @@ -1985,6 +2192,20 @@ "typedarray": "^0.0.6" } }, + "configstore": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", + "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", + "dev": true, + "requires": { + "dot-prop": "^4.1.0", + "graceful-fs": "^4.1.2", + "make-dir": "^1.0.0", + "unique-string": "^1.0.0", + "write-file-atomic": "^2.0.0", + "xdg-basedir": "^3.0.0" + } + }, "connect": { "version": "3.6.6", "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", @@ -2186,6 +2407,15 @@ "elliptic": "^6.0.0" } }, + "create-error-class": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", + "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", + "dev": true, + "requires": { + "capture-stack-trace": "^1.0.0" + } + }, "create-hash": { "version": "1.2.0", "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", @@ -2252,6 +2482,12 @@ "randomfill": "^1.0.3" } }, + "crypto-random-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", + "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", + "dev": true + }, "css-parse": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-1.7.0.tgz", @@ -2370,6 +2606,12 @@ "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", "dev": true }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true + }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -2666,6 +2908,15 @@ "domelementtype": "1" } }, + "dot-prop": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", + "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "dev": true, + "requires": { + "is-obj": "^1.0.0" + } + }, "duplexer2": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", @@ -2701,6 +2952,12 @@ } } }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "dev": true + }, "duplexify": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.0.tgz", @@ -3349,6 +3606,24 @@ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", "dev": true }, + "body-parser": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", + "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", + "dev": true, + "requires": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.1", + "http-errors": "~1.6.2", + "iconv-lite": "0.4.19", + "on-finished": "~2.3.0", + "qs": "6.5.1", + "raw-body": "2.3.2", + "type-is": "~1.6.15" + } + }, "qs": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", @@ -4345,6 +4620,15 @@ "is-glob": "^2.0.0" } }, + "global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "dev": true, + "requires": { + "ini": "^1.3.4" + } + }, "globals": { "version": "9.18.0", "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", @@ -4386,6 +4670,25 @@ "sparkles": "^1.0.0" } }, + "got": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", + "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", + "dev": true, + "requires": { + "create-error-class": "^3.0.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "is-redirect": "^1.0.0", + "is-retry-allowed": "^1.0.0", + "is-stream": "^1.0.0", + "lowercase-keys": "^1.0.0", + "safe-buffer": "^5.0.1", + "timed-out": "^4.0.0", + "unzip-response": "^2.0.1", + "url-parse-lax": "^1.0.0" + } + }, "graceful-fs": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", @@ -4886,6 +5189,12 @@ "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", "dev": true }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", + "dev": true + }, "image-size": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", @@ -4911,6 +5220,12 @@ "resolve-from": "^3.0.0" } }, + "import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "dev": true + }, "import-local": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-1.0.0.tgz", @@ -5054,6 +5369,15 @@ "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", "dev": true }, + "is-ci": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", + "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", + "dev": true, + "requires": { + "ci-info": "^1.5.0" + } + }, "is-data-descriptor": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", @@ -5140,6 +5464,22 @@ "is-extglob": "^1.0.0" } }, + "is-installed-globally": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", + "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", + "dev": true, + "requires": { + "global-dirs": "^0.1.0", + "is-path-inside": "^1.0.0" + } + }, + "is-npm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", + "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", + "dev": true + }, "is-number": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", @@ -5148,6 +5488,12 @@ "kind-of": "^3.0.2" } }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true + }, "is-path-cwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", @@ -5197,6 +5543,12 @@ "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=" }, + "is-redirect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", + "dev": true + }, "is-regex": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", @@ -5206,6 +5558,12 @@ "has": "^1.0.1" } }, + "is-retry-allowed": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", + "dev": true + }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", @@ -5706,6 +6064,15 @@ "is-buffer": "^1.1.5" } }, + "latest-version": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", + "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", + "dev": true, + "requires": { + "package-json": "^4.0.0" + } + }, "lazy-cache": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", @@ -6050,6 +6417,12 @@ "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=", "dev": true }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true + }, "lru-cache": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", @@ -6858,51 +7231,481 @@ } } }, - "nopt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", - "dev": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "is-builtin-module": "^1.0.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "requires": { - "remove-trailing-separator": "^1.0.1" - } - }, - "normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", - "dev": true - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "nodemon": { + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.19.4.tgz", + "integrity": "sha512-VGPaqQBNk193lrJFotBU8nvWZPqEZY2eIzymy2jjY0fJ9qIsxA0sxQ8ATPl0gZC645gijYEc1jtZvpS8QWzJGQ==", "dev": true, "requires": { - "path-key": "^2.0.0" - } - }, + "chokidar": "^2.1.8", + "debug": "^3.2.6", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.0.4", + "pstree.remy": "^1.1.7", + "semver": "^5.7.1", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.2", + "update-notifier": "^2.5.0" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true + } + } + }, + "nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "dev": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "is-builtin-module": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "dev": true + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", @@ -7193,6 +7996,18 @@ "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", "dev": true }, + "package-json": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", + "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", + "dev": true, + "requires": { + "got": "^6.7.1", + "registry-auth-token": "^3.0.1", + "registry-url": "^3.0.3", + "semver": "^5.1.0" + } + }, "pako": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", @@ -7523,6 +8338,12 @@ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "dev": true + }, "preserve": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", @@ -7650,6 +8471,12 @@ "dev": true, "optional": true }, + "pstree.remy": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.7.tgz", + "integrity": "sha512-xsMgrUwRpuGskEzBFkH8NmTimbZ5PcPup0LA8JJkHIm2IMUbQcpo3yeLNWVrufEYjh8YwtSVh0xz6UeWc5Oh5A==", + "dev": true + }, "public-encrypt": { "version": "4.0.2", "resolved": "http://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz", @@ -7816,6 +8643,18 @@ "integrity": "sha1-DD0L6u2KAclm2Xh793goElKpeao=", "dev": true }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, "read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -8206,6 +9045,25 @@ "safe-regex": "^1.1.0" } }, + "registry-auth-token": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", + "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", + "dev": true, + "requires": { + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", + "dev": true, + "requires": { + "rc": "^1.0.1" + } + }, "relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", @@ -8670,6 +9528,15 @@ "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==", "dev": true }, + "semver-diff": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", + "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", + "dev": true, + "requires": { + "semver": "^5.0.3" + } + }, "semver-intersect": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/semver-intersect/-/semver-intersect-1.4.0.tgz", @@ -9543,6 +10410,15 @@ "inherits": "2" } }, + "term-size": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", + "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", + "dev": true, + "requires": { + "execa": "^0.7.0" + } + }, "through2": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", @@ -9565,6 +10441,12 @@ "integrity": "sha512-lJbq6KsFhZJtN3fPUVje1tq/hHsJOKUUcUj/MGCiQR6qWBDcyi5kxL9J7/RnaEChCn0+L/DUN2WvemDrkk4i3Q==", "dev": true }, + "timed-out": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", + "dev": true + }, "timers-browserify": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz", @@ -9639,12 +10521,38 @@ } } }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "dev": true + }, "toposort": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz", "integrity": "sha1-LmhELZ9k7HILjMieZEOsbKqVACk=", "dev": true }, + "touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "requires": { + "nopt": "~1.0.10" + }, + "dependencies": { + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dev": true, + "requires": { + "abbrev": "1" + } + } + } + }, "tough-cookie": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", @@ -9923,6 +10831,15 @@ "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=", "dev": true }, + "undefsafe": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.2.tgz", + "integrity": "sha1-Il9rngM3Zj4Njnz9aG/Cg2zKznY=", + "dev": true, + "requires": { + "debug": "^2.2.0" + } + }, "underscore.string": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.5.tgz", @@ -9962,6 +10879,15 @@ "imurmurhash": "^0.1.4" } }, + "unique-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", + "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", + "dev": true, + "requires": { + "crypto-random-string": "^1.0.0" + } + }, "universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -10015,12 +10941,36 @@ } } }, + "unzip-response": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", + "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", + "dev": true + }, "upath": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==", "dev": true }, + "update-notifier": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", + "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", + "dev": true, + "requires": { + "boxen": "^1.2.1", + "chalk": "^2.0.1", + "configstore": "^3.0.0", + "import-lazy": "^2.1.0", + "is-ci": "^1.0.10", + "is-installed-globally": "^0.1.0", + "is-npm": "^1.0.0", + "latest-version": "^3.0.0", + "semver-diff": "^2.0.0", + "xdg-basedir": "^3.0.0" + } + }, "upper-case": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", @@ -10101,6 +11051,15 @@ "requires-port": "^1.0.0" } }, + "url-parse-lax": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", + "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", + "dev": true, + "requires": { + "prepend-http": "^1.0.1" + } + }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", @@ -11633,6 +12592,48 @@ "string-width": "^1.0.2 || 2" } }, + "widest-line": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", + "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", + "dev": true, + "requires": { + "string-width": "^2.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, "window-size": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", @@ -11670,6 +12671,17 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, + "write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, "ws": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.5.tgz", @@ -11680,6 +12692,12 @@ "ultron": "1.0.x" } }, + "xdg-basedir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", + "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", + "dev": true + }, "xhr2": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.1.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index 2bbc7a06..ad1aee58 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,21 +7,25 @@ "start": "ng serve --proxy proxy.conf.json", "start-qa": "ng serve --environment=qa", "start-prod": "ng serve --environment=prod", + "local-proxy": "ng serve --environment=local --proxy local.proxy.conf.json", "build": "ng build --prod --sourcemap false", "build-token": "ng build --token --sourcemap false", "lint": "tslint \"src/**/*.ts\"", "test": "ng test", "pree2e": "webdriver-manager update", - "e2e": "protractor" + "e2e": "protractor", + "mock-server": "nodemon src\\mock-server\\server.js" }, "private": true, "dependencies": { "@angular/animations": "^5.2.11", + "@angular/cdk": "^5.0.0", "@angular/common": "^5.2.11", "@angular/compiler": "^5.2.11", "@angular/compiler-cli": "^5.2.11", "@angular/core": "^5.2.11", "@angular/forms": "^5.2.11", + "@angular/material": "^5.0.0", "@angular/platform-browser": "^5.2.11", "@angular/platform-browser-dynamic": "^5.2.11", "@angular/platform-server": "^5.2.11", @@ -50,6 +54,7 @@ "@angular/cli": "^1.1.2", "@types/jasmine": "^2.8.16", "@types/node": "^6.14.7", + "body-parser": "^1.19.0", "codelyzer": "1.0.0-beta.1", "jasmine-core": "^2.4.1", "jasmine-spec-reporter": "^2.5.0", @@ -58,6 +63,7 @@ "karma-cli": "^1.0.1", "karma-jasmine": "^1.0.2", "karma-remap-istanbul": "^0.2.1", + "nodemon": "^1.19.0", "protractor": "4.0.9", "ts-node": "1.2.1", "tslint": "3.13.0", diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 7d105ef0..c8901508 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -14,6 +14,7 @@ import {ObserversService} from './services/observers.service'; import {ToastrModule} from 'ngx-toastr'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import { NotificationsService } from './services/notifications.service'; +import {EditableFormsService} from './services/editable.forms.service'; export function HttpLoaderFactory(httpClient: HttpClient) { return new TranslateHttpLoader(httpClient); @@ -36,9 +37,11 @@ export function HttpLoaderFactory(httpClient: HttpClient) { useFactory: HttpLoaderFactory, deps: [HttpClient] } - }) + }), + BrowserAnimationsModule ], providers: [ + EditableFormsService, ObserversService, NotificationsService ], diff --git a/frontend/src/app/components/components.module.ts b/frontend/src/app/components/components.module.ts index ae0b8207..3a7f59a7 100644 --- a/frontend/src/app/components/components.module.ts +++ b/frontend/src/app/components/components.module.ts @@ -19,9 +19,17 @@ import { ObserverProfileComponent } from './observers/observer-profile/observer- import { NotificationsComponent } from './notifications/notifications.component'; import { ObserverTileComponent } from './notifications/observer-tile/observer-tile.component'; import { NgMultiSelectDropDownModule } from 'ng-multiselect-dropdown'; +import {EditableFormsComponent} from './editable-forms/editable-forms.component'; +import {EditableFormSectionsComponent} from './editable-forms/editable-form-sections/editable-form-sections.component'; +import {FormSectionMenuComponent} from './editable-forms/editable-form-sections/form-section-menu/form-section-menu.component'; +import {FormSectionCardComponent} from './editable-forms/editable-form-sections/form-section-card/form-section-card.component'; +import {QuestionCardComponent} from './editable-forms/form-section-questions/question-card/question-card.component'; +import {FormSectionQuestionsComponent} from './editable-forms/form-section-questions/form-section-questions.component'; +import {QuestionMenuComponent} from './editable-forms/form-section-questions/question-menu/question-menu.component'; +import {MatAutocompleteModule, MatButtonModule, MatCheckboxModule, MatInputModule, MatSelectModule} from '@angular/material'; export let components = [ - AnswerComponent, + AnswerComponent, AnswerListComponent, AnswerDetailsComponent, AnswerFormListComponent, @@ -38,15 +46,37 @@ export let components = [ StatisticsValueComponent, NotificationsComponent, ObserverTileComponent, - LoginComponent -] + LoginComponent, + EditableFormsComponent, + EditableFormSectionsComponent, + FormSectionMenuComponent, + FormSectionCardComponent, + FormSectionQuestionsComponent, + QuestionCardComponent, + QuestionMenuComponent +]; @NgModule({ - declarations:components, - exports: components, - imports:[SharedModule, NgMultiSelectDropDownModule.forRoot()] + declarations: components, + exports: [ + ...components, + MatSelectModule, + MatInputModule, + MatButtonModule, + MatAutocompleteModule, + MatCheckboxModule + ], + imports: [ + SharedModule, + NgMultiSelectDropDownModule.forRoot(), + MatSelectModule, + MatInputModule, + MatButtonModule, + MatAutocompleteModule, + MatCheckboxModule + ] }) -export class ComponentsModule { - +export class ComponentsModule { + } diff --git a/frontend/src/app/components/editable-forms/editable-form-sections/editable-form-sections.component.html b/frontend/src/app/components/editable-forms/editable-form-sections/editable-form-sections.component.html new file mode 100644 index 00000000..57696564 --- /dev/null +++ b/frontend/src/app/components/editable-forms/editable-form-sections/editable-form-sections.component.html @@ -0,0 +1,38 @@ +
+
+

{{ 'FORM' | translate }} - {{selectedFormSet.code}}

+ + + + +
+
+
+
+
+

{{ 'FORM' | translate }} - {{section.code}}

+

{{section.description}}

+
+
+
View
+
+
+
Edit
+
+
+
Delete
+
+
+
+
+

{{'ADD_NEW_FORM' | translate}}

+
+
+
+
+
+
+

{{'ADD_NEW_FORM' | translate}}

+
+
+
diff --git a/frontend/src/app/components/editable-forms/editable-form-sections/editable-form-sections.component.scss b/frontend/src/app/components/editable-forms/editable-form-sections/editable-form-sections.component.scss new file mode 100644 index 00000000..80af04fe --- /dev/null +++ b/frontend/src/app/components/editable-forms/editable-form-sections/editable-form-sections.component.scss @@ -0,0 +1,46 @@ +@import '~variables.scss'; +.published{ + background: $color-primary; +} +.draft{ + background: $color-info; +} +.new{ + background: $color-success; + color: $color-white; +} +.clickable { + text-align: center; + -webkit-user-select: none; /* Safari 3.1+ */ + -moz-user-select: none; /* Firefox 2+ */ + -ms-user-select: none; /* IE 10+ */ + user-select: none; /* Standard syntax */ + cursor: pointer; +} +.form-sections-container{ + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; + grid-auto-rows: auto; + grid-gap: 20px; +} +.form-section-card-container{ + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 120px 30px; + grid-gap: 10px; +} +.form-section-card{ + @extend .clickable; + grid-column: 1 / 4; + grid-row: 1 / 2; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +} +.form-section-menu{ + @extend .clickable; + text-align: center; + color: $color-white; + background-color: $color-success; +} diff --git a/frontend/src/app/components/editable-forms/editable-form-sections/editable-form-sections.component.ts b/frontend/src/app/components/editable-forms/editable-form-sections/editable-form-sections.component.ts new file mode 100644 index 00000000..eda19f36 --- /dev/null +++ b/frontend/src/app/components/editable-forms/editable-form-sections/editable-form-sections.component.ts @@ -0,0 +1,48 @@ +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {ActivatedRoute, Params} from '@angular/router'; +import {Store} from '@ngrx/store'; +import {AppState} from '../../../store/store.module'; +import {Subscription} from 'rxjs'; +import {EditableFormsAddFormToSetAction, EditableFormsDeleteFormAction,} from '../../../store/editable-forms/editable.forms.actions'; +import {EditableForm} from '../../../models/editable.form.model'; + +@Component({ + selector: 'app-editable-form-sections', + templateUrl: './editable-form-sections.component.html', + styleUrls: ['./editable-form-sections.component.scss'] +}) +export class EditableFormSectionsComponent implements OnInit, OnDestroy { + private subs: Subscription[] = []; + selectedFormSet: EditableForm; + + constructor(private store: Store, + private activeRoute: ActivatedRoute) { + } + + ngOnInit() { + this.subs.push( + this.activeRoute.params + .switchMap((params: Params) => this.store + .select(s => s.editableForms.forms) + .concatMap( forms => forms) + .filter( form => form.id === parseInt(params.id)) + ) + .subscribe(selectedFormSet => this.selectedFormSet = selectedFormSet) + ); + } + + ngOnDestroy() { + this.subs.forEach(s => s.unsubscribe()); + } + + addNewForm() { + this.store.dispatch(new EditableFormsAddFormToSetAction(this.selectedFormSet)); + } + + deleteFormSection(formSectionId: number) { + this.store.dispatch(new EditableFormsDeleteFormAction({ + formSet: this.selectedFormSet, + formId: formSectionId + })); + } +} diff --git a/frontend/src/app/components/editable-forms/editable-form-sections/form-section-card/form-section-card.component.html b/frontend/src/app/components/editable-forms/editable-form-sections/form-section-card/form-section-card.component.html new file mode 100644 index 00000000..6a6fa2a3 --- /dev/null +++ b/frontend/src/app/components/editable-forms/editable-form-sections/form-section-card/form-section-card.component.html @@ -0,0 +1,7 @@ +
+
+

{{title}}

+
{{subtitle}}
+
+ +
diff --git a/frontend/src/app/components/editable-forms/editable-form-sections/form-section-card/form-section-card.component.scss b/frontend/src/app/components/editable-forms/editable-form-sections/form-section-card/form-section-card.component.scss new file mode 100644 index 00000000..336b9b76 --- /dev/null +++ b/frontend/src/app/components/editable-forms/editable-form-sections/form-section-card/form-section-card.component.scss @@ -0,0 +1,13 @@ +@import "~variables.scss"; + +.card-container{ + +} +.card{ + min-height: 80px; + text-align: center; +} +.new{ + color: $color-white; + background-color: $color-success; +} diff --git a/frontend/src/app/components/editable-forms/editable-form-sections/form-section-card/form-section-card.component.ts b/frontend/src/app/components/editable-forms/editable-form-sections/form-section-card/form-section-card.component.ts new file mode 100644 index 00000000..0fde4320 --- /dev/null +++ b/frontend/src/app/components/editable-forms/editable-form-sections/form-section-card/form-section-card.component.ts @@ -0,0 +1,17 @@ +import {Component, Input, OnInit} from '@angular/core'; + +@Component({ + selector: 'app-form-section-card', + templateUrl: './form-section-card.component.html', + styleUrls: ['./form-section-card.component.scss'] +}) +export class FormSectionCardComponent implements OnInit { + @Input() title: string; + @Input() subtitle: string; + @Input() hasMenu: boolean = false; + constructor() { } + + ngOnInit() { + } + +} diff --git a/frontend/src/app/components/editable-forms/editable-form-sections/form-section-menu/form-section-menu.component.html b/frontend/src/app/components/editable-forms/editable-form-sections/form-section-menu/form-section-menu.component.html new file mode 100644 index 00000000..f04ea8d8 --- /dev/null +++ b/frontend/src/app/components/editable-forms/editable-form-sections/form-section-menu/form-section-menu.component.html @@ -0,0 +1,11 @@ + diff --git a/frontend/src/app/components/editable-forms/editable-form-sections/form-section-menu/form-section-menu.component.scss b/frontend/src/app/components/editable-forms/editable-form-sections/form-section-menu/form-section-menu.component.scss new file mode 100644 index 00000000..fc59a687 --- /dev/null +++ b/frontend/src/app/components/editable-forms/editable-form-sections/form-section-menu/form-section-menu.component.scss @@ -0,0 +1,13 @@ +@import "~variables.scss"; +.menu-container{ + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: auto; + grid-gap: 10px; +} +.menu-card{ + text-align: center; + padding: 0; + color: $color-white; + background-color: $color-success; +} diff --git a/frontend/src/app/components/editable-forms/editable-form-sections/form-section-menu/form-section-menu.component.ts b/frontend/src/app/components/editable-forms/editable-form-sections/form-section-menu/form-section-menu.component.ts new file mode 100644 index 00000000..7dd839bf --- /dev/null +++ b/frontend/src/app/components/editable-forms/editable-form-sections/form-section-menu/form-section-menu.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-form-section-menu', + templateUrl: './form-section-menu.component.html', + styleUrls: ['./form-section-menu.component.scss'] +}) +export class FormSectionMenuComponent implements OnInit { + + constructor() { } + + ngOnInit() { + } + +} diff --git a/frontend/src/app/components/editable-forms/editable-forms.component.html b/frontend/src/app/components/editable-forms/editable-forms.component.html new file mode 100644 index 00000000..4c0e4930 --- /dev/null +++ b/frontend/src/app/components/editable-forms/editable-forms.component.html @@ -0,0 +1,11 @@ +
+
+

{{ 'ADD_NEW_FORM_SET' | translate }}

+
+
+

{{ 'FORM' | translate }} - {{editableForm.code}}

+

{{editableForm.description}}

+
+
diff --git a/frontend/src/app/components/editable-forms/editable-forms.component.scss b/frontend/src/app/components/editable-forms/editable-forms.component.scss new file mode 100644 index 00000000..7af7b2f5 --- /dev/null +++ b/frontend/src/app/components/editable-forms/editable-forms.component.scss @@ -0,0 +1,32 @@ +@import '~variables.scss'; +.published{ + background: $color-primary; +} +.draft{ + background: $color-info; +} +.new{ + background: $color-success; + color: $color-white; +} +.clickable { + -webkit-user-select: none; /* Safari 3.1+ */ + -moz-user-select: none; /* Firefox 2+ */ + -ms-user-select: none; /* IE 10+ */ + user-select: none; /* Standard syntax */ + cursor: pointer; +} +.form-sets-container{ + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; + grid-auto-rows: 120px; + grid-gap: 20px; +} +.card{ + @extend .clickable; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; +} diff --git a/frontend/src/app/components/editable-forms/editable-forms.component.ts b/frontend/src/app/components/editable-forms/editable-forms.component.ts new file mode 100644 index 00000000..f86a80c5 --- /dev/null +++ b/frontend/src/app/components/editable-forms/editable-forms.component.ts @@ -0,0 +1,38 @@ +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {EditableForm} from '../../models/editable.form.model'; +import {Subscription} from 'rxjs'; +import {Store} from '@ngrx/store'; +import {AppState} from '../../store/store.module'; +import {EditableFormsCreateAction} from '../../store/editable-forms/editable.forms.actions'; + +const draftsFirst = (left, right) => { + return left.published ? 1 : right.published ? -1 : 0; +}; + +@Component({ + selector: 'app-editable-forms', + templateUrl: './editable-forms.component.html', + styleUrls: ['./editable-forms.component.scss'] +}) +export class EditableFormsComponent implements OnInit, OnDestroy { + editableForms: EditableForm[]; + sub: Subscription; + + constructor(private store: Store) { + } + + ngOnInit() { + this.sub = this.store.select(s => s.editableForms.forms) + .subscribe(forms => { + this.editableForms = forms.sort(draftsFirst); + }); + } + + ngOnDestroy() { + this.sub.unsubscribe(); + } + + createNewFormSet() { + this.store.dispatch(new EditableFormsCreateAction()); + } +} diff --git a/frontend/src/app/components/editable-forms/form-section-questions/form-section-questions.component.html b/frontend/src/app/components/editable-forms/form-section-questions/form-section-questions.component.html new file mode 100644 index 00000000..04697cb4 --- /dev/null +++ b/frontend/src/app/components/editable-forms/form-section-questions/form-section-questions.component.html @@ -0,0 +1,35 @@ +
+
+

{{ 'FORM' | translate }} - {{section.code}}

+ + + + +
+
+ +
+
+ + +
+ + +
+
+ + +
diff --git a/frontend/src/app/components/editable-forms/form-section-questions/form-section-questions.component.scss b/frontend/src/app/components/editable-forms/form-section-questions/form-section-questions.component.scss new file mode 100644 index 00000000..b21a3952 --- /dev/null +++ b/frontend/src/app/components/editable-forms/form-section-questions/form-section-questions.component.scss @@ -0,0 +1,29 @@ +@import "~variables.scss"; +.page{ + display: grid; + grid-template-columns: 1fr auto; + grid-gap: 10px; + align-items: flex-end; + margin-bottom: 20px; +} +.card-container{ + display: flex; + flex-wrap: nowrap; + flex-direction: column; + width: 100%; +} +.question-card{ + margin-bottom: 20px; + &:last-of-type{ + margin-bottom: 0; + } +} +@media screen and (max-width: 768px) { + .page{ + grid-template-columns: 1fr; + grid-template-rows: auto; + } + .question-menu-container{ + padding-left: 45px; + } +} diff --git a/frontend/src/app/components/editable-forms/form-section-questions/form-section-questions.component.ts b/frontend/src/app/components/editable-forms/form-section-questions/form-section-questions.component.ts new file mode 100644 index 00000000..ea6968ef --- /dev/null +++ b/frontend/src/app/components/editable-forms/form-section-questions/form-section-questions.component.ts @@ -0,0 +1,95 @@ +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {EditableFormSection} from '../../../models/editable.form.section.model'; +import {Store} from '@ngrx/store'; +import {AppState} from '../../../store/store.module'; +import {Subscription} from 'rxjs'; +import {ActivatedRoute, Params} from '@angular/router'; +import { + EditableFormsAddFormQuestionAction, + EditableFormsDeleteFormQuestionAction, + EditableFormsSaveFormSectionAction, + EditableFormsSaveOptionsAction +} from '../../../store/editable-forms/editable.forms.actions'; +import {EditableForm} from '../../../models/editable.form.model'; + +@Component({ + selector: 'app-form-section-questions', + templateUrl: './form-section-questions.component.html', + styleUrls: ['./form-section-questions.component.scss'] +}) +export class FormSectionQuestionsComponent implements OnInit, OnDestroy { + private formSet: EditableForm; + private section: EditableFormSection; + private subs: Subscription[] = []; + private editMode: boolean = false; + + constructor(private store: Store, + private activeRoute: ActivatedRoute) { + } + + ngOnInit() { + this.subs.push( + this.loadFormSet(), + this.loadFormSection(), + this.loadEditMode() + ); + } + + private loadFormSet = () => { + return this.activeRoute.params + .switchMap((params: Params) => { + console.log('Params for questions got changed:', params); + return this.store.select(s => s.editableForms.forms) + .concatMap(forms => forms) + .filter( f => f.id === parseInt(params.formSetId)) + }) + .subscribe(selectedFormSet => { + this.formSet = selectedFormSet; + }) + }; + + private loadFormSection = () => { + return this.activeRoute.params + .switchMap((params: Params) => { + console.log('Params for questions got changed:', params); + return this.store.select(s => s.editableForms.forms) + .concatMap(forms => forms) + .filter(f => f.id === parseInt(params.formSetId)) + .flatMap(forms => forms.sections) + .filter(section => section.uniqueId === params.formId); + }) + .subscribe(selectedFormSection => { + console.log('We received a form: ', selectedFormSection); + this.section = selectedFormSection; + }) + }; + + private loadEditMode = () => { + return this.activeRoute.queryParams.subscribe( + (params: Params) => this.editMode = params.edit === 'true'); + }; + + onAddQuestion() { + this.store.dispatch(new EditableFormsAddFormQuestionAction({ + formSet: this.formSet, + formId: this.section.id + })); + } + + onSaveFormSection(){ + this.store.dispatch(new EditableFormsSaveFormSectionAction(this.formSet)); + this.store.dispatch(new EditableFormsSaveOptionsAction(this.formSet)); + } + + onDeleteQuestion(questionId) { + this.store.dispatch(new EditableFormsDeleteFormQuestionAction({ + formSet: this.formSet, + formId: this.section.id, + questionId: questionId + })); + } + + ngOnDestroy() { + this.subs.forEach(s => s.unsubscribe()); + } +} diff --git a/frontend/src/app/components/editable-forms/form-section-questions/question-card/question-card.component.html b/frontend/src/app/components/editable-forms/form-section-questions/question-card/question-card.component.html new file mode 100644 index 00000000..14a5964d --- /dev/null +++ b/frontend/src/app/components/editable-forms/form-section-questions/question-card/question-card.component.html @@ -0,0 +1,40 @@ +
+
+ +
+
+
+ + + +

{{question.text}}

+
+
+ + + + + {{option.text}} + + + + + + + + Red flag +
+
+ +
+
+ + + + {{type.text}} + + + +
+
+
diff --git a/frontend/src/app/components/editable-forms/form-section-questions/question-card/question-card.component.scss b/frontend/src/app/components/editable-forms/form-section-questions/question-card/question-card.component.scss new file mode 100644 index 00000000..4af2fbeb --- /dev/null +++ b/frontend/src/app/components/editable-forms/form-section-questions/question-card/question-card.component.scss @@ -0,0 +1,55 @@ +@import "~variables.scss"; +.question-row{ + display: flex; + align-items: center; +} +.question-delete{ + width: 36px; + height: 40px; + border-radius: 4px; + &:hover{ + background-color: $color-light-gray; + } + margin-right: 10px; +} +.question-delete > i { + position: relative; + top: 6px; + left: 6px; +} +.question-body{ + width: 100%; + display: grid; + grid-template-columns: 1fr auto; + grid-gap: 10px; + grid-template-rows: auto; + padding: 0 10px; + box-shadow: 0 1px 3px 0 #A0A0A0; +} +.question-side-options{ + padding: 10px 0 0 0; + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: flex-end; + + select{ + border: 0; + font-size: 16px; + border-bottom: 2px dashed $color-secondary; + } +} +.question-header{ + width: 100%; +} +.option-row-container{ + width: 100%; + display: grid; + grid-template-columns: 20px 1fr 1fr 90px; + grid-gap: 10px; + align-items: baseline; + grid-template-rows: auto; +} +.question-options-last-item{ + justify-self: end; +} diff --git a/frontend/src/app/components/editable-forms/form-section-questions/question-card/question-card.component.ts b/frontend/src/app/components/editable-forms/form-section-questions/question-card/question-card.component.ts new file mode 100644 index 00000000..7712b114 --- /dev/null +++ b/frontend/src/app/components/editable-forms/form-section-questions/question-card/question-card.component.ts @@ -0,0 +1,64 @@ +import {Component, EventEmitter, Input, OnDestroy, OnInit, Output} from '@angular/core'; +import {EditableFormQuestion} from '../../../../models/editable.form.question.model'; +import {EditableFormSection} from '../../../../models/editable.form.section.model'; +import {QuestionType} from '../../../../models/editable.form.question.type'; +import {EditableFormQuestionOption} from '../../../../models/editable.form.question.option.model'; +import {Subscription} from 'rxjs'; +import {Store} from '@ngrx/store'; +import {AppState} from '../../../../store/store.module'; +import {TranslateService} from '@ngx-translate/core'; + +@Component({ + selector: 'app-question-card', + templateUrl: './question-card.component.html', + styleUrls: ['./question-card.component.scss'] +}) +export class QuestionCardComponent implements OnInit, OnDestroy { + @Input() private section: EditableFormSection; + @Input() private question: EditableFormQuestion; + @Input() private editMode: boolean = false; + @Output() deleteQuestion = new EventEmitter(); + + private questionTypes:QuestionType[] = QuestionType.values(); + private ADD_NEW_OPTION_ID = -999; + + private options: EditableFormQuestionOption[]; + private sub: Subscription; + constructor(private store: Store, private translate: TranslateService) { + } + + ngOnInit() { + this.sub = this.store.select(s => s.editableForms.options) + .subscribe(newOptionTypes => { + this.question.options.forEach(questionOption => { + let optionTypeIndex = newOptionTypes.findIndex(optionType => optionType.text === questionOption.text); + if (optionTypeIndex >= 0){ + console.log(`Question option ${questionOption.id} has the following type id: ${newOptionTypes[optionTypeIndex].id}`); + questionOption.id = newOptionTypes[optionTypeIndex].id; + }else{ + console.log(`We couldn't find the type for option: ${questionOption.text}`); + } + }); + this.options = [ + new EditableFormQuestionOption(this.ADD_NEW_OPTION_ID, this.translate.instant('ADD_NEW_OPTION'), false), + ...newOptionTypes + ]; + }); + } + + ngOnDestroy(): void { + this.sub.unsubscribe(); + } + + onDeleteQuestion(){ + this.deleteQuestion.emit(this.question.id); + } + + onAddClick(){ + this.question.options.push(new EditableFormQuestionOption(111, "New option", false, true)); + } + + onOptionTypeChange(event){ + console.log(`Question option type has changed!`, event); + } +} diff --git a/frontend/src/app/components/editable-forms/form-section-questions/question-menu/question-menu.component.html b/frontend/src/app/components/editable-forms/form-section-questions/question-menu/question-menu.component.html new file mode 100644 index 00000000..fe73b1fe --- /dev/null +++ b/frontend/src/app/components/editable-forms/form-section-questions/question-menu/question-menu.component.html @@ -0,0 +1,14 @@ + diff --git a/frontend/src/app/components/editable-forms/form-section-questions/question-menu/question-menu.component.scss b/frontend/src/app/components/editable-forms/form-section-questions/question-menu/question-menu.component.scss new file mode 100644 index 00000000..467d2690 --- /dev/null +++ b/frontend/src/app/components/editable-forms/form-section-questions/question-menu/question-menu.component.scss @@ -0,0 +1,36 @@ +@import "~variables.scss"; +.clickable { + text-align: center; + -webkit-user-select: none; /* Safari 3.1+ */ + -moz-user-select: none; /* Firefox 2+ */ + -ms-user-select: none; /* IE 10+ */ + user-select: none; /* Standard syntax */ + cursor: pointer; +} +.menu-container{ + display: grid; + grid-template-columns: 1fr 1fr; + grid-auto-rows: auto; + grid-gap: 10px; +} +.menu-item{ + @extend .clickable; + min-width: 100px; + text-align: center; + color: $color-white; + background-color: $color-success; + h6{ + padding: 10px; + } +} +@media screen and (max-width: 768px) { + .menu-container{ + grid-template-columns: 1fr 1fr 1fr 1fr; + } +} + +@media screen and (max-width: 992px) { + .menu-container{ + grid-template-columns: 1fr; + } +} diff --git a/frontend/src/app/components/editable-forms/form-section-questions/question-menu/question-menu.component.ts b/frontend/src/app/components/editable-forms/form-section-questions/question-menu/question-menu.component.ts new file mode 100644 index 00000000..bbf8cdb5 --- /dev/null +++ b/frontend/src/app/components/editable-forms/form-section-questions/question-menu/question-menu.component.ts @@ -0,0 +1,34 @@ +import {Component, EventEmitter, OnInit, Output} from '@angular/core'; + +@Component({ + selector: 'app-question-menu', + templateUrl: './question-menu.component.html', + styleUrls: ['./question-menu.component.scss'] +}) +export class QuestionMenuComponent implements OnInit { + @Output() addQuestion = new EventEmitter(); + @Output() saveFormSection = new EventEmitter(); + constructor() { } + + ngOnInit() { + } + + addNewFormSection() { + console.log('new form section'); + } + + addNewQuestion(){ + this.addQuestion.emit(); + } + + saveAsDraft() { + console.log('save as draft'); + this.saveFormSection.emit(); + } + + publishForm() { + console.log('publish the form'); + this.saveFormSection.emit(); + } + +} diff --git a/frontend/src/app/components/header/header.component.html b/frontend/src/app/components/header/header.component.html index 1cf9ac70..0333b82a 100644 --- a/frontend/src/app/components/header/header.component.html +++ b/frontend/src/app/components/header/header.component.html @@ -47,6 +47,11 @@ {{'NOTIFICATIONS' | translate}} +
  • + + {{'FORMS' | translate}} + +
  • diff --git a/frontend/src/app/models/editable.form.model.ts b/frontend/src/app/models/editable.form.model.ts new file mode 100644 index 00000000..1c1ac387 --- /dev/null +++ b/frontend/src/app/models/editable.form.model.ts @@ -0,0 +1,13 @@ +// This should be merged with form.model.ts +import {EditableFormSection} from './editable.form.section.model'; + +export class EditableForm{ + constructor( + public id: number, + public code: string, + public sections: EditableFormSection[] = [], + public description: string = '', + public version: number = 1, + public published: boolean = false + ) {} +} diff --git a/frontend/src/app/models/editable.form.question.model.ts b/frontend/src/app/models/editable.form.question.model.ts new file mode 100644 index 00000000..1cdd5fad --- /dev/null +++ b/frontend/src/app/models/editable.form.question.model.ts @@ -0,0 +1,12 @@ +import {EditableFormQuestionOption} from './editable.form.question.option.model'; + +export class EditableFormQuestion{ + constructor( + public id: number, + public formId: number, + public code: string, + public text: string, + public typeId: number, + public options: EditableFormQuestionOption[] = [] + ) {} +} diff --git a/frontend/src/app/models/editable.form.question.option.model.ts b/frontend/src/app/models/editable.form.question.option.model.ts new file mode 100644 index 00000000..e905f41e --- /dev/null +++ b/frontend/src/app/models/editable.form.question.option.model.ts @@ -0,0 +1,8 @@ +export class EditableFormQuestionOption{ + constructor( + public id: number, + public text: string, + public isTextOption: boolean, + public isFlagged: boolean = false + ) {} +} diff --git a/frontend/src/app/models/editable.form.question.type.ts b/frontend/src/app/models/editable.form.question.type.ts new file mode 100644 index 00000000..79d76597 --- /dev/null +++ b/frontend/src/app/models/editable.form.question.type.ts @@ -0,0 +1,12 @@ +export class QuestionType{ + static readonly SINGLE_CHOICE = new QuestionType(1, 'Single choice'); + static readonly MULTIPLE_CHOICE = new QuestionType(0, 'Multiple choice'); + static readonly SINGLE_CHOICE_TEXT = new QuestionType(2, 'Single choice with text'); + static readonly MULTIPLE_CHOICE_TEXT = new QuestionType(3, 'Multiple choice with text'); + + static values():QuestionType[]{ + return [QuestionType.SINGLE_CHOICE, QuestionType.MULTIPLE_CHOICE, QuestionType.SINGLE_CHOICE_TEXT, QuestionType.MULTIPLE_CHOICE_TEXT]; + } + + constructor(public id: number, public text: string){}; +} diff --git a/frontend/src/app/models/editable.form.section.model.ts b/frontend/src/app/models/editable.form.section.model.ts new file mode 100644 index 00000000..0de0e440 --- /dev/null +++ b/frontend/src/app/models/editable.form.section.model.ts @@ -0,0 +1,11 @@ +import {EditableFormQuestion} from './editable.form.question.model'; + +export class EditableFormSection{ + constructor( + public id: number, + public code: string, + public uniqueId: string, + public description: string = '', + public questions: EditableFormQuestion[] = [] + ) {} +} diff --git a/frontend/src/app/models/form.model.ts b/frontend/src/app/models/form.model.ts index f7178712..c298e082 100644 --- a/frontend/src/app/models/form.model.ts +++ b/frontend/src/app/models/form.model.ts @@ -2,4 +2,4 @@ import { FormSection } from './form.section.model'; export class Form { idFormular: number; sections: FormSection[] -} \ No newline at end of file +} diff --git a/frontend/src/app/routing/app.routes.ts b/frontend/src/app/routing/app.routes.ts index a4d12db3..089d0166 100644 --- a/frontend/src/app/routing/app.routes.ts +++ b/frontend/src/app/routing/app.routes.ts @@ -1,16 +1,22 @@ -import { AnonGuard } from '../core/anon.guard'; -import { LoginComponent } from '../components/login/login.component'; -import { AuthGuard } from '../core/authGuard/auth.guard'; -import { StatisticsDetailsComponent } from '../components/statistics/statistics-details/statistics-details.component'; +import {AnonGuard} from '../core/anon.guard'; +import {LoginComponent} from '../components/login/login.component'; +import {AuthGuard} from '../core/authGuard/auth.guard'; +import {StatisticsDetailsComponent} from '../components/statistics/statistics-details/statistics-details.component'; +import {AnswerDetailsComponent} from '../components/answer/answer-details/answer-details.component'; +import {LoadStatisticsGuard} from './guards/load-statistics.guard'; +import {AnswerDetailsGuard} from './guards/load-anwer-details.guard'; +import {AnswerListGuard} from './guards/load-answer-list.guard'; +import {HomeGuard} from './guards/home.guard'; +import {StatisticsComponent} from '../components/statistics/statistics.component'; +import {AnswerComponent} from '../components/answer/answer.component'; +import {Routes} from '@angular/router'; +import {EditableFormsComponent} from '../components/editable-forms/editable-forms.component'; +import {EditableFormsGuard} from './guards/editable-forms.guard'; +import {EditableFormSectionsGuard} from './guards/editable-form-sections.guard'; +import {EditableFormSectionsComponent} from '../components/editable-forms/editable-form-sections/editable-form-sections.component'; +import {FormSectionQuestionsComponent} from '../components/editable-forms/form-section-questions/form-section-questions.component'; +import {EditableFormSectionQuestionsGuard} from './guards/editable-form-section-questions.guard'; import { ObserversComponent } from '../components/observers/observers.component'; -import { AnswerDetailsComponent } from '../components/answer/answer-details/answer-details.component'; -import { LoadStatisticsGuard } from './guards/load-statistics.guard'; -import { AnswerDetailsGuard } from './guards/load-anwer-details.guard'; -import { AnswerListGuard } from './guards/load-answer-list.guard'; -import { HomeGuard } from './guards/home.guard'; -import { StatisticsComponent } from '..//components/statistics/statistics.component'; -import { AnswerComponent } from '..//components/answer/answer.component'; -import { Routes } from '@angular/router'; import { ObserverProfileComponent } from 'app/components/observers/observer-profile/observer-profile.component'; import { NotificationsComponent } from 'app/components/notifications/notifications.component'; @@ -86,5 +92,17 @@ export let appRoutes: Routes = [ path: 'notifications', component: NotificationsComponent, canActivate: [AuthGuard] - }, -] + }, { + path: 'forms', + canActivate: [AuthGuard, EditableFormsGuard], + component: EditableFormsComponent + }, { + path: 'forms/:id', + canActivate: [AuthGuard, EditableFormSectionsGuard], + component: EditableFormSectionsComponent + }, { + path: 'forms/:formSetId/:formId/questions', + canActivate: [AuthGuard, EditableFormSectionQuestionsGuard], + component: FormSectionQuestionsComponent + } +]; diff --git a/frontend/src/app/routing/app.routing.module.ts b/frontend/src/app/routing/app.routing.module.ts index 110c4993..d9a1fb85 100644 --- a/frontend/src/app/routing/app.routing.module.ts +++ b/frontend/src/app/routing/app.routing.module.ts @@ -1,11 +1,14 @@ -import { environment } from '../../environments/environment'; -import { appRoutes } from './app.routes'; -import { RouterModule } from '@angular/router'; -import { LoadStatisticsGuard } from './guards/load-statistics.guard'; -import { AnswerDetailsGuard } from './guards/load-anwer-details.guard'; -import { AnswerListGuard } from './guards/load-answer-list.guard'; -import { HomeGuard } from './guards/home.guard'; -import { NgModule } from '@angular/core'; +import {appRoutes} from './app.routes'; +import {RouterModule} from '@angular/router'; +import {LoadStatisticsGuard} from './guards/load-statistics.guard'; +import {AnswerDetailsGuard} from './guards/load-anwer-details.guard'; +import {AnswerListGuard} from './guards/load-answer-list.guard'; +import {HomeGuard} from './guards/home.guard'; +import {NgModule} from '@angular/core'; +import {EditableFormsGuard} from './guards/editable-forms.guard'; +import {EditableFormSectionsGuard} from './guards/editable-form-sections.guard'; +import {EditableFormSectionQuestionsGuard} from './guards/editable-form-section-questions.guard'; + @NgModule({ imports: [ RouterModule.forRoot(appRoutes,{ @@ -13,7 +16,8 @@ import { NgModule } from '@angular/core'; // enableTracing: !environment.production }) ], - providers: [HomeGuard, AnswerListGuard, AnswerDetailsGuard, LoadStatisticsGuard] + providers: [HomeGuard, AnswerListGuard, AnswerDetailsGuard, LoadStatisticsGuard, + EditableFormsGuard, EditableFormSectionsGuard, EditableFormSectionQuestionsGuard] }) export class AppRoutingModule { diff --git a/frontend/src/app/routing/guards/editable-form-section-questions.guard.ts b/frontend/src/app/routing/guards/editable-form-section-questions.guard.ts new file mode 100644 index 00000000..921bd281 --- /dev/null +++ b/frontend/src/app/routing/guards/editable-form-section-questions.guard.ts @@ -0,0 +1,14 @@ +import {Injectable} from '@angular/core'; +import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router'; +import {Store} from '@ngrx/store'; +import {AppState} from '../../store/store.module'; +import {EditableFormsLoadAllOptionsAction} from '../../store/editable-forms/editable.forms.actions'; + +@Injectable() +export class EditableFormSectionQuestionsGuard implements CanActivate{ + constructor(private store: Store) {} + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { + this.store.dispatch(new EditableFormsLoadAllOptionsAction()); + return true; + } +} diff --git a/frontend/src/app/routing/guards/editable-form-sections.guard.ts b/frontend/src/app/routing/guards/editable-form-sections.guard.ts new file mode 100644 index 00000000..72413f11 --- /dev/null +++ b/frontend/src/app/routing/guards/editable-form-sections.guard.ts @@ -0,0 +1,14 @@ +import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router'; +import {Store} from '@ngrx/store'; +import {AppState} from '../../store/store.module'; +import {EditableFormsLoadByIdAction} from '../../store/editable-forms/editable.forms.actions'; +import {Injectable} from '@angular/core'; + +@Injectable() +export class EditableFormSectionsGuard implements CanActivate{ + constructor(private store: Store) {} + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { + this.store.dispatch(new EditableFormsLoadByIdAction(route.params.id)); + return true; + } +} diff --git a/frontend/src/app/routing/guards/editable-forms.guard.ts b/frontend/src/app/routing/guards/editable-forms.guard.ts new file mode 100644 index 00000000..daafb4f4 --- /dev/null +++ b/frontend/src/app/routing/guards/editable-forms.guard.ts @@ -0,0 +1,14 @@ +import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router'; +import {Injectable} from '@angular/core'; +import {Store} from '@ngrx/store'; +import {AppState} from '../../store/store.module'; +import {EditableFormsLoadAllAction} from '../../store/editable-forms/editable.forms.actions'; + +@Injectable() +export class EditableFormsGuard implements CanActivate { + constructor(private store: Store) {} + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { + this.store.dispatch(new EditableFormsLoadAllAction()); + return true; + } +} diff --git a/frontend/src/app/services/editable.forms.service.ts b/frontend/src/app/services/editable.forms.service.ts new file mode 100644 index 00000000..00b0cccf --- /dev/null +++ b/frontend/src/app/services/editable.forms.service.ts @@ -0,0 +1,124 @@ +import {Injectable} from '@angular/core'; +import {ApiService} from '../core/apiService/api.service'; +import {EditableForm} from '../models/editable.form.model'; +import {EditableFormSection} from '../models/editable.form.section.model'; +import {EditableFormQuestion} from '../models/editable.form.question.model'; +import {EditableFormQuestionOption} from '../models/editable.form.question.option.model'; +import {Observable} from 'rxjs'; +import {environment} from '../../environments/environment'; + +const API = { + forms: (id = undefined) => id ? `/api/v1/form/${id}` : '/api/v1/form', + options: (id = undefined) => id ? `/api/v1/option/${id}` : '/api/v1/option', + createOption: () => `/api/v1/option/create` +}; + +@Injectable() +export class EditableFormsService { + + private baseUrl: string; + constructor(private http: ApiService) { + this.baseUrl = environment.apiUrl; + } + + public loadAllForms(): Observable { + return this.http.get(this.baseUrl + API.forms()) + .flatMap(versions => versions.formVersions) + .map(f => new EditableForm(f.id, f.code, f.formSections || [], f.description, f.ver)) + .toArray(); + }; + + public loadFormById(id: string): Observable { + return this.http.get(this.baseUrl + API.forms(id)) + .map((sections: BackendFormSection[]) => ( + (sections || []).map(s => new EditableFormSection(s.id, s.code, s.uniqueId, s.description, + (s.questions || []).map((q: BackendFormQuestion) => new EditableFormQuestion(q.id, q.idSection, q.code, q.text, q.questionType, + (q.optionsToQuestions || []).map((o: BackendFormQuestionOption) => new EditableFormQuestionOption(o.idOption, o.text, o.isFreeText))))) + )) + ); + } + + public loadAllFormsOptions(): Observable { + return this.http.get(this.baseUrl + API.options()) + .map((options: BackendFormOption[]) => ( + options.map(o => new EditableFormQuestionOption(o.id, o.text, o.isFreeText)) + )); + }; + + public saveOption(option: EditableFormQuestionOption): Observable { + console.log(`Saving new option: ${option.text}`, option); + return this.http.post(this.baseUrl + API.createOption(), { + text: option.text + }) + .map( (o: BackendFormOption) => new EditableFormQuestionOption(o.id, o.text, o.isFreeText)); + } + + public saveFormSet(formSet: EditableForm): Observable{ + let form: BackendForm = new BackendForm(formSet.code, formSet.description, + formSet.sections.map(section => new BackendFormSection(section.code, section.description, + section.questions.map(question => new BackendFormQuestion(formSet.code, question.code, question.typeId, question.text, + question.options.map(option => new BackendFormQuestionOption(option.id))))))); + console.log(`Saving the form ${formSet}: with the following serialization:\n`, JSON.stringify(form, null, 2)); + return this.http.post(this.baseUrl + API.forms(), form); + } +} + +class BackendFormOption{ + constructor( + public id: number, + public text: string, + public isFreeText: boolean, + public hint: string + ){} +} + +class BackendFormQuestionOption { + constructor( + public idOption: number, + public text: string = undefined, + public isFreeText: boolean = false + ) { + } +} + +class BackendFormQuestion { + constructor( + public formCode: string, + public code: string, + public questionType: number, + public text: string, + public optionsToQuestions: BackendFormQuestionOption[], + public id: number = undefined, + public idSection:number = undefined + ) { + } +} + +class BackendFormSection { + constructor( + public code: string, + public description: string, + public questions: any[], + public id: number = undefined, + public uniqueId: string = undefined + ) { + } +} + +class BackendForm { + constructor( + public code: string, + public description: string, + public formSections: BackendFormSection[], + public id: number = undefined, + public ver: number = undefined + ) { + } +} + +class FormVersions { + constructor( + public formVersions: BackendForm[] + ) { + } +} diff --git a/frontend/src/app/shared/functions.ts b/frontend/src/app/shared/functions.ts new file mode 100644 index 00000000..53f08616 --- /dev/null +++ b/frontend/src/app/shared/functions.ts @@ -0,0 +1,11 @@ +export const replaceAt = (array: any[], index: number, item: any) => ( + (index < 0 || index >= array.length) ? (array) : ([ + ...array.slice(0, index), + item, + ...array.slice(index + 1) + ]) +); +export const decode = (encoded) => (parseInt(encoded.length === 1 ? 0 : encoded.charAt(1), 0) * 26 + + encoded.charCodeAt(0) - 'A'.charCodeAt(0)); +export const encode = (numeric) => (String.fromCharCode(numeric % 26 + 'A'.charCodeAt(0)) + + (Math.floor(numeric / 26) === 0 ? '' : Math.floor(numeric / 26))); diff --git a/frontend/src/app/shared/id.service.ts b/frontend/src/app/shared/id.service.ts new file mode 100644 index 00000000..18b292ea --- /dev/null +++ b/frontend/src/app/shared/id.service.ts @@ -0,0 +1,12 @@ +const decode = (encoded) => parseInt(encoded.charAt(1)) * 26 + encoded.charCodeAt(0) - 'A'.charCodeAt(0); +const encode = (numeric) => String.fromCharCode(numeric % 26 + 'A'.charCodeAt(0)) + Math.floor(numeric / 26); +export function nextEncodedId(encodedIds){ + const maxId = Math.max(...encodedIds + .map(id => id.length === 1 ? id + '0' : id) + .map(decode), 0); + return encode(maxId + 1); +} +export function nextNumericId(ids){ + const maxId = Math.max(...ids, 0); + return maxId + 1; +} diff --git a/frontend/src/app/store/editable-forms/editable.forms.actions.ts b/frontend/src/app/store/editable-forms/editable.forms.actions.ts new file mode 100644 index 00000000..dcbe3160 --- /dev/null +++ b/frontend/src/app/store/editable-forms/editable.forms.actions.ts @@ -0,0 +1,104 @@ +import {actionType} from '../util'; +import {Action} from '@ngrx/store'; +import {EditableForm} from '../../models/editable.form.model'; +import {EditableFormSection} from '../../models/editable.form.section.model'; +import {EditableFormQuestionOption} from '../../models/editable.form.question.option.model'; + +export class EditableFormsActionTypes{ + static readonly LOAD_ALL = actionType('[Editable Forms] LOAD ALL'); + static readonly LOAD_ALL_COMPLETE = actionType('[Editable Forms] LOAD ALL COMPLETE'); + static readonly LOAD_BY_ID = actionType('[Editable Forms] LOAD BY ID'); + static readonly LOAD_BY_ID_COMPLETE = actionType('[Editable Forms] LOAD BY ID COMPLETE'); + static readonly CREATE_FORM_SET = actionType('[Editable Forms] CREATE FORM SET'); + static readonly CREATE_FORM_SET_COMPLETE = actionType('[Editable Forms] CREATE FORM SET COMPLETE'); + static readonly ADD_FORM_TO_SET = actionType('[Editable Forms] ADD FORM TO SET'); + static readonly DELETE_FORM_FROM_SET = actionType('[Editable Forms] DELETE FORM FROM SET'); + static readonly ADD_QUESTION_TO_FORM = actionType('[Editable Forms] ADD QUESTION TO FORM'); + static readonly DELETE_QUESTION_FROM_FORM = actionType('[Editable Forms] DELETE QUESTION FROM FORM'); + static readonly SAVE_FORM_SET = actionType('[Editable Forms] SAVE FORM SET'); + static readonly SAVE_FORM_SET_COMPLETE = actionType('[Editable Forms] SAVE FORM SET COMPLETE'); + // options + static readonly LOAD_ALL_OPTIONS = actionType('[Editable Forms] LOAD ALL OPTIONS'); + static readonly LOAD_ALL_OPTIONS_COMPLETE = actionType('[Editable Forms] LOAD ALL OPTIONS COMPLETE'); + static readonly LOAD_OPTIONS_BY_ID_COMPLETE = actionType('[Editable Forms] LOAD OPTION BY ID COMPLETE'); + static readonly SAVE_OPTIONS = actionType('[Editable Forms] SAVE OPTIONS'); +} +export class EditableFormsLoadAllAction implements Action{ + readonly type = EditableFormsActionTypes.LOAD_ALL; +} +export class EditableFormsLoadAllCompleteAction implements Action{ + readonly type = EditableFormsActionTypes.LOAD_ALL_COMPLETE; + constructor(public payload: EditableForm[]){} +} +export class EditableFormsLoadByIdAction implements Action{ + readonly type = EditableFormsActionTypes.LOAD_BY_ID; + constructor(public payload: string){} +} +export class EditableFormsLoadByIdCompleteAction implements Action { + readonly type = EditableFormsActionTypes.LOAD_BY_ID_COMPLETE; + constructor(public payload: EditableFormSection[]){} +} +export class EditableFormsCreateAction implements Action{ + readonly type = EditableFormsActionTypes.CREATE_FORM_SET; +} +export class EditableFormsCreateCompleteAction implements Action{ + readonly type = EditableFormsActionTypes.CREATE_FORM_SET_COMPLETE; + constructor(public payload: EditableForm) {} +} +export class EditableFormsAddFormToSetAction implements Action{ + readonly type = EditableFormsActionTypes.ADD_FORM_TO_SET; + constructor(public payload: EditableForm) {} +} +export class EditableFormsDeleteFormAction implements Action{ + readonly type = EditableFormsActionTypes.DELETE_FORM_FROM_SET; + constructor(public payload: {formSet: EditableForm, formId: number}) {} +} +export class EditableFormsAddFormQuestionAction implements Action{ + readonly type = EditableFormsActionTypes.ADD_QUESTION_TO_FORM; + constructor(public payload: {formSet: EditableForm, formId: number}) {} +} +export class EditableFormsDeleteFormQuestionAction implements Action{ + readonly type = EditableFormsActionTypes.DELETE_QUESTION_FROM_FORM; + constructor(public payload: {formSet: EditableForm, formId: number, questionId: number}) {} +} +export class EditableFormsSaveFormSectionAction implements Action{ + readonly type = EditableFormsActionTypes.SAVE_FORM_SET; + constructor(public payload: EditableForm) {} +} +export class EditableFormsSaveFormSectionCompleteAction implements Action{ + readonly type = EditableFormsActionTypes.SAVE_FORM_SET_COMPLETE; +} +// options +export class EditableFormsLoadAllOptionsAction implements Action{ + readonly type = EditableFormsActionTypes.LOAD_ALL_OPTIONS; +} +export class EditableFormsLoadAllOptionsCompleteAction implements Action{ + readonly type = EditableFormsActionTypes.LOAD_ALL_OPTIONS_COMPLETE; + constructor(public payload: EditableFormQuestionOption[]){} +} +export class EditableFormsLoadOptionByIdCompleteAction implements Action{ + readonly type = EditableFormsActionTypes.LOAD_OPTIONS_BY_ID_COMPLETE; + constructor(public payload: EditableFormQuestionOption){} +} +export class EditableFormsSaveOptionsAction implements Action{ + readonly type = EditableFormsActionTypes.SAVE_OPTIONS; + constructor(public payload: EditableForm) {} +} +export type EditableFormsActions = EditableFormsLoadAllAction + | EditableFormsLoadAllCompleteAction + | EditableFormsLoadByIdAction + | EditableFormsLoadByIdCompleteAction + | EditableFormsCreateAction + | EditableFormsAddFormToSetAction + | EditableFormsDeleteFormAction + | EditableFormsAddFormQuestionAction + | EditableFormsDeleteFormQuestionAction + | EditableFormsCreateCompleteAction + | EditableFormsSaveFormSectionAction + | EditableFormsSaveFormSectionCompleteAction + // options + | EditableFormsLoadAllOptionsAction + | EditableFormsLoadAllOptionsCompleteAction + | EditableFormsLoadOptionByIdCompleteAction + | EditableFormsSaveOptionsAction + ; diff --git a/frontend/src/app/store/editable-forms/editable.forms.effects.ts b/frontend/src/app/store/editable-forms/editable.forms.effects.ts new file mode 100644 index 00000000..12574537 --- /dev/null +++ b/frontend/src/app/store/editable-forms/editable.forms.effects.ts @@ -0,0 +1,78 @@ +import {Injectable} from '@angular/core'; +import {Actions, Effect} from '@ngrx/effects'; +import { + EditableFormsActionTypes, + EditableFormsLoadAllCompleteAction, + EditableFormsLoadAllOptionsCompleteAction, + EditableFormsLoadByIdAction, + EditableFormsLoadByIdCompleteAction, + EditableFormsLoadOptionByIdCompleteAction, + EditableFormsSaveFormSectionAction, + EditableFormsSaveFormSectionCompleteAction +} from './editable.forms.actions'; +import {EditableFormsService} from '../../services/editable.forms.service'; +import {EditableForm} from '../../models/editable.form.model'; +import {EditableFormSection} from '../../models/editable.form.section.model'; +import {EditableFormQuestionOption} from '../../models/editable.form.question.option.model'; +import {EditableFormQuestion} from '../../models/editable.form.question.model'; +import {Store} from '@ngrx/store'; +import {AppState} from '../store.module'; + +@Injectable() +export class EditableFormsEffects { + constructor(private actions: Actions, private store: Store, private service: EditableFormsService) { + } + + @Effect() + loadEditableFormsAction = this.actions + .ofType(EditableFormsActionTypes.LOAD_ALL) + .concatMap(() => this.service.loadAllForms()) + .map((forms: EditableForm[]) => new EditableFormsLoadAllCompleteAction(forms)); + + @Effect() + loadEditableFormsOptionsAction = this.actions + .ofType(EditableFormsActionTypes.LOAD_ALL_OPTIONS) + .concatMap(() => this.service.loadAllFormsOptions()) + .map((forms: EditableFormQuestionOption[]) => new EditableFormsLoadAllOptionsCompleteAction(forms)); + + @Effect() + loadEditableFormsByIdAction = this.actions + .ofType(EditableFormsActionTypes.LOAD_BY_ID) + .map((action: EditableFormsLoadByIdAction) => action.payload) + .concatMap(id => this.service.loadFormById(id)) + .map((sections: EditableFormSection[]) => new EditableFormsLoadByIdCompleteAction(sections)); + + @Effect() + saveOptions = this.actions + .ofType(EditableFormsActionTypes.SAVE_OPTIONS) + .map((action: EditableFormsSaveFormSectionAction) => action.payload) + .flatMap((formSet: EditableForm) => formSet.sections) + .flatMap((section: EditableFormSection) => section.questions) + .flatMap((question: EditableFormQuestion) => question.options) + .filter((option: EditableFormQuestionOption) => option.id < 0) + .concatMap((option: EditableFormQuestionOption) => this.service.saveOption(option)) + .map((savedOption: EditableFormQuestionOption) => new EditableFormsLoadOptionByIdCompleteAction(savedOption)); + + @Effect() + saveFormSet = this.actions + .withLatestFrom(this.store.select(s => s.editableForms.savingForm)) + .filter((value: [EditableFormsSaveFormSectionAction, EditableForm]) => { + let formSet: EditableForm = value[1]; + let notSavedOptions: number = 0; + (formSet == undefined ? [] : formSet.sections) + .forEach((section: EditableFormSection) => { + section.questions.forEach((question: EditableFormQuestion) => { + question.options.forEach((option: EditableFormQuestionOption) => { + notSavedOptions += (option.id < 0 ? 1 : 0); + }) + }) + }); + return formSet != undefined && notSavedOptions === 0; + }) + .do(([, formSet]) => {console.log(`Trying to save form set: ${formSet.description}`, formSet)} ) + .switchMap(([, formSet]) => this.service.saveFormSet(formSet)) + .switchMap((savedId) => [ + new EditableFormsSaveFormSectionCompleteAction(), + new EditableFormsLoadByIdAction(savedId) + ]) +} diff --git a/frontend/src/app/store/editable-forms/editable.forms.reducer.ts b/frontend/src/app/store/editable-forms/editable.forms.reducer.ts new file mode 100644 index 00000000..61699a0f --- /dev/null +++ b/frontend/src/app/store/editable-forms/editable.forms.reducer.ts @@ -0,0 +1,230 @@ +import {EditableForm} from '../../models/editable.form.model'; +import {EditableFormsActions, EditableFormsActionTypes} from './editable.forms.actions'; +import {decode, encode, replaceAt} from '../../shared/functions'; +import {EditableFormSection} from '../../models/editable.form.section.model'; +import {EditableFormQuestionOption} from '../../models/editable.form.question.option.model'; +import {EditableFormQuestion} from '../../models/editable.form.question.model'; +import {QuestionType} from '../../models/editable.form.question.type'; + +export class EditableFormsState { + forms: EditableForm[]; + options: EditableFormQuestionOption[]; + savingForm: EditableForm; +} + +const initialState: EditableFormsState = { + forms: [], + options: [], + savingForm: undefined +}; + +export function editableFormsReducer(state = initialState, $action: EditableFormsActions) { + switch ($action.type) { + case EditableFormsActionTypes.LOAD_ALL_COMPLETE: + return loadAllComplete(state, $action.payload); + case EditableFormsActionTypes.LOAD_BY_ID_COMPLETE: + return loadedFormSectionComplete(state, $action.payload); + case EditableFormsActionTypes.CREATE_FORM_SET_COMPLETE: + return createdFormSet(state, $action); + case EditableFormsActionTypes.ADD_FORM_TO_SET: + return addNewFormSection(state, $action.payload); + case EditableFormsActionTypes.DELETE_FORM_FROM_SET: + return deleteFormSection(state, $action.payload); + case EditableFormsActionTypes.CREATE_FORM_SET: + return createNewForm(state); + case EditableFormsActionTypes.ADD_QUESTION_TO_FORM: + return addNewQuestionToFormSection(state, $action.payload); + case EditableFormsActionTypes.DELETE_QUESTION_FROM_FORM: + return deleteQuestionFromSection(state, $action.payload); + // options + case EditableFormsActionTypes.LOAD_ALL_OPTIONS_COMPLETE: + return loadAllOptionsComplete(state, $action.payload); + case EditableFormsActionTypes.LOAD_OPTIONS_BY_ID_COMPLETE: + return loadOptionByIdComplete(state, $action.payload); + // save hack + case EditableFormsActionTypes.SAVE_FORM_SET: + return startSavingForm(state, $action.payload); + case EditableFormsActionTypes.SAVE_FORM_SET_COMPLETE: + return finishSavingForm(state); + default: + return state; + } +} + +const loadAllComplete = (state: EditableFormsState, formSets: EditableForm[]) => { + const existingFormSetIds = state.forms.map(f => f.code); + const newFormSets = formSets.filter(f => !existingFormSetIds.find(code => code === f.code)); + return { + ...state, + forms: [ + ...state.forms, + ...newFormSets + ] + } +}; + +const loadedFormSectionComplete = (state, sections: EditableFormSection[]) => { + let formCode: string = sections + .filter(section => section.code) + .map(section => section.code) + .find(() => true); + if (formCode){ + let editedFormIndex: number = state.forms.findIndex(f => f.code === formCode); + if (editedFormIndex >= 0){ + let savedSections = sections.filter(s => s.id >= 0); + let existingForm: EditableForm = state.forms[editedFormIndex]; + let futureForm: EditableForm = new EditableForm(existingForm.id, existingForm.code, savedSections, + existingForm.description, existingForm.version, existingForm.published); + console.log(`Replacing existing form(${formCode}) with a new one that has the following sections: `, savedSections); + return { + ...state, + forms: replaceAt(state.forms, editedFormIndex, futureForm) + } + }else{ + console.log(`There is no Form with the code: ${formCode}`); + return state; + } + }else{ + console.log(`There is no code defined on the form sections received: `, sections); + return state; + } +}; + +const createdFormSet = (state, $action) => { + return { + ...state, + forms: [$action.payload, ...state.forms] + } +}; + + +const createNewForm = (state) => { + let newForm: EditableForm = new EditableForm(findNextId(state.forms), findNextCode(state.forms), [], 'This is new form'); + return { + ...state, + forms: [ + ...state.forms, + newForm + ] + } +}; + +const addNewFormSection = (state, formSet: EditableForm) => { + let existingFormIndex: number = state.forms.findIndex(f => f.id === formSet.id); + let updatedFormSections: EditableFormSection[] = [ + ...formSet.sections, + new EditableFormSection(findNextId(formSet.sections), formSet.code, findNextUniqueId(formSet.sections), 'This is a new section') + ]; + let updatedFormSet: EditableForm = new EditableForm(formSet.id, formSet.code, updatedFormSections, + formSet.description, formSet.version, formSet.published); + return { + ...state, + forms: replaceAt(state.forms, existingFormIndex, updatedFormSet) + } +}; + +const deleteFormSection = (state, {formSet, formId}) => { + let existingFormIndex: number = state.forms.findIndex(f => f.id === formSet.id); + let existingForm: EditableForm = state.forms[existingFormIndex]; + let updatedForm: EditableForm = new EditableForm(existingForm.id, existingForm.code, existingForm.sections.filter(s => s.id !== formId), + existingForm.description, existingForm.version, existingForm.published); + return { + ...state, + forms: replaceAt(state.forms, existingFormIndex, updatedForm) + }; +}; + +const addNewQuestionToFormSection = (state, {formSet, formId}) => { + let existingFormIndex: number = state.forms.findIndex(f => f.id === formSet.id); + let existingFormSet: EditableForm = state.forms[existingFormIndex]; + let existingFormSectionIndex: number = existingFormSet.sections.findIndex(s => s.id === formId); + let existingFormSection: EditableFormSection = existingFormSet.sections[existingFormSectionIndex]; + let updateFormSection: EditableFormSection = new EditableFormSection(existingFormSection.id, existingFormSection.code, existingFormSection.uniqueId, + existingFormSection.description, [ + ...existingFormSection.questions, + new EditableFormQuestion(findNextId(existingFormSection.questions), formId, existingFormSet.code, 'This is new question', QuestionType.SINGLE_CHOICE.id) + ]); + let updatedFormSet: EditableForm = new EditableForm(formSet.id, formSet.code, + replaceAt(existingFormSet.sections, existingFormSectionIndex, updateFormSection), + formSet.description, formSet.version, formSet.published); + return { + ...state, + forms: replaceAt(state.forms, existingFormIndex, updatedFormSet) + }; +}; + +const deleteQuestionFromSection = (state, {formSet, formId, questionId}) => { + let existingFormIndex: number = state.forms.findIndex(f => f.id === formSet.id); + let existingFormSet: EditableForm = state.forms[existingFormIndex]; + let existingFormSectionIndex: number = existingFormSet.sections.findIndex(s => s.id === formId); + let existingFormSection: EditableFormSection = existingFormSet.sections[existingFormSectionIndex]; + let updateFormSection: EditableFormSection = new EditableFormSection(existingFormSection.id, existingFormSection.code, existingFormSection.uniqueId, + existingFormSection.description, existingFormSection.questions.filter(q => q.id !== questionId)); + let updatedFormSet: EditableForm = new EditableForm(formSet.id, formSet.code, + replaceAt(existingFormSet.sections, existingFormSectionIndex, updateFormSection), + formSet.description, formSet.version, formSet.published); + return { + ...state, + forms: replaceAt(state.forms, existingFormIndex, updatedFormSet) + }; +}; + +const startSavingForm = (state, formSet: EditableForm) => { + return { + ...state, + savingForm: formSet + } +}; + +const finishSavingForm = (state) => { + return { + ...state, + savingForm: undefined + } +}; + +const loadAllOptionsComplete = (state, options: EditableFormQuestionOption[]) => { + return { + ...state, + options + }; +}; + +const loadOptionByIdComplete = (state, option: EditableFormQuestionOption) => { + let savingFormIndex: number = state.forms.findIndex(f => f.id === state.savingForm.id); + let updatedSavingForm: EditableForm = state.savingForm; + state.savingForm.sections.forEach((section: EditableFormSection, sectionIndex: number) => { + section.questions.forEach((question: EditableFormQuestion, questionIndex: number) => { + let optionIndex:number = question.options.findIndex(o => o.text === option.text); + if (optionIndex >= 0){ + let updatedQuestion: EditableFormQuestion = new EditableFormQuestion(question.id, question.formId, question.code, question.text, + question.typeId, replaceAt(question.options, optionIndex, option)); + let updatedSection: EditableFormSection = new EditableFormSection(section.id, section.code, section.uniqueId, section.description, + replaceAt(section.questions, questionIndex, updatedQuestion)); + updatedSavingForm = new EditableForm(state.savingForm.id, state.savingForm.code, replaceAt(state.savingForm.sections, sectionIndex, updatedSection), + state.savingForm.description, state.savingForm.version, state.savingForm.published); + } + }) + }); + return { + ...state, + forms: replaceAt(state.forms, savingFormIndex, updatedSavingForm), + options: [ + ...state.options, + option + ], + savingForm: updatedSavingForm + }; +}; + +const findNextId = (items) => ( items.map(i => i.id).reduce((max, val) => Math.max(max, val), 0) + 1 ); +const findNextCode = (items) => ( encode(items + .map(f => f.code) + .map(code => decode(code)) + .filter(x => !isNaN(x)) + .reduce((max, val) => Math.max(max, val), 0) + 1)); +const findNextUniqueId = (items) => (encode(items + .map(f => f.uniqueId) + .map(code => decode(code)) + .filter(x => !isNaN(x)) + .reduce((max, val) => Math.max(max, val), 0) + 1)); diff --git a/frontend/src/app/store/store.module.ts b/frontend/src/app/store/store.module.ts index 0e047ad1..53131425 100644 --- a/frontend/src/app/store/store.module.ts +++ b/frontend/src/app/store/store.module.ts @@ -17,6 +17,8 @@ import { StoreDevtoolsModule } from '@ngrx/store-devtools'; import { ObserversState, ObserversCountState } from './observers/observers.state'; import {ObserversEffects, ObserversCountEffects} from './observers/observers.effects'; import {observersReducer, observersCountReducer} from './observers/observers.reducer'; +import {editableFormsReducer, EditableFormsState} from './editable-forms/editable.forms.reducer'; +import {EditableFormsEffects} from './editable-forms/editable.forms.effects'; export class AppState { form: FormState; @@ -24,18 +26,20 @@ export class AppState { statistics: StatisticsState; observers: ObserversState; observersCount: ObserversCountState; - note: NoteState + note: NoteState; + editableForms: EditableFormsState; } let moduleImports = [ - StoreModule.forRoot({ form: formReducer, answer: answerReducer, statistics: statisticsReducer, observers: observersReducer, note: noteReducer , observersCount: observersCountReducer}), + StoreModule.forRoot({ form: formReducer, answer: answerReducer, statistics: statisticsReducer, observers: observersReducer, note: noteReducer , observersCount: observersCountReducer, editableForms: editableFormsReducer}), EffectsModule.forRoot([ FormEffects, AnswerEffects, StatisticsEffects, ObserversEffects, ObserversCountEffects, - NoteEffects + NoteEffects, + EditableFormsEffects ]), ]; if (!environment.production) { diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json index 7e81524c..7669670a 100644 --- a/frontend/src/assets/i18n/en.json +++ b/frontend/src/assets/i18n/en.json @@ -40,5 +40,12 @@ "DESELECT_ALL": "Deselect all", "SELECTED_OBSERVERS_COUNT":"Number of active observers:", "SEND_NOTIFICATIONS_TO": "Send notification to", - "RESET": "Clear filter" + "RESET": "Clear filter", + "FORMS": "Forms", + "ADD_NEW_FORM_SET": "Add new form set", + "ADD_NEW_FORM": "Add new form", + "ADD_NEW_QUESTION": "Add new question", + "SAVE_AS_DRAFT": "Save as draft", + "PUBLISH_FORM": "Publish form", + "ADD_NEW_OPTION": "Add new option" } diff --git a/frontend/src/assets/i18n/ro.json b/frontend/src/assets/i18n/ro.json index fad06cfa..4f2d29e2 100644 --- a/frontend/src/assets/i18n/ro.json +++ b/frontend/src/assets/i18n/ro.json @@ -25,5 +25,12 @@ "FILTER_BY": "Filtreaza după:", "COUNTY_CODE": "Județ:", "OBSERVER_ID": "Id-ul observatorului:", - "POLLING_STATION_NUMBER": "Numărul secției de votare:" + "POLLING_STATION_NUMBER": "Numărul secției de votare:", + "FORMS": "Formulare", + "ADD_NEW_FORM_SET": "Adauga formular nou", + "ADD_NEW_FORM": "Adauga sectiune noua", + "ADD_NEW_QUESTION": "Adauga intrebare", + "SAVE_AS_DRAFT": "Salveaza temporar", + "PUBLISH_FORM": "Publica formular", + "ADD_NEW_OPTION": "Adauga optiune noua" } diff --git a/frontend/src/environments/environment.local.ts b/frontend/src/environments/environment.local.ts new file mode 100644 index 00000000..e694b25f --- /dev/null +++ b/frontend/src/environments/environment.local.ts @@ -0,0 +1,12 @@ +import { EnvironmentConfig } from 'typings'; + +// The file contents for the current environment will overwrite these during build. +// The build system defaults to the dev environment which uses `environment.ts`, but if you do +// `ng build --env=prod` then `environment.prod.ts` will be used instead. +// The list of which env maps to which file can be found in `angular-cli.json`. + +export const environment:EnvironmentConfig = { + production: false, + observerGuideUrl: 'http://monitorizare-vot-ghid.azurewebsites.net/', + apiUrl: '' +}; diff --git a/frontend/src/environments/environment.ts b/frontend/src/environments/environment.ts index dcfa6b8d..e694b25f 100644 --- a/frontend/src/environments/environment.ts +++ b/frontend/src/environments/environment.ts @@ -8,5 +8,5 @@ import { EnvironmentConfig } from 'typings'; export const environment:EnvironmentConfig = { production: false, observerGuideUrl: 'http://monitorizare-vot-ghid.azurewebsites.net/', - apiUrl: 'https://mv-mobile-test.azurewebsites.net' + apiUrl: '' }; diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index 26ae3fd8..619ede6a 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -1,7 +1,7 @@ @import '~variables.scss'; @import '~styles/shared.scss'; @import '~styles/toastr.scss'; - +@import "~@angular/material/prebuilt-themes/deeppurple-amber.css"; .nav-tabs > li > a.active { color: $color-secondary !important; diff --git a/frontend/src/variables.scss b/frontend/src/variables.scss index 14e2db46..7c09fb56 100644 --- a/frontend/src/variables.scss +++ b/frontend/src/variables.scss @@ -6,3 +6,7 @@ $color-danger: #fd0001; $color-light-gray: #ddd2e7; $color-gray: #7b8184; $color-white: white; +$color-success: #2ec122; +$color-info: #44dcfd; + +$form-focus-size: 1;