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