mirror of
https://github.com/vale981/Vulcan
synced 2025-03-05 09:31:43 -05:00
Merge branch 'devel' of github.com:VulcanJS/Vulcan into devel
This commit is contained in:
commit
e93f4062d8
149 changed files with 7958 additions and 686 deletions
|
@ -54,6 +54,7 @@
|
||||||
"single",
|
"single",
|
||||||
"avoid-escape"
|
"avoid-escape"
|
||||||
],
|
],
|
||||||
|
"react/display-name": 1,
|
||||||
"react/prop-types": 0,
|
"react/prop-types": 0,
|
||||||
"semi": [1, "always"]
|
"semi": [1, "always"]
|
||||||
},
|
},
|
||||||
|
|
272
package-lock.json
generated
272
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "Vulcan",
|
"name": "Vulcan",
|
||||||
"version": "1.12.16",
|
"version": "1.12.17",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -330,13 +330,22 @@
|
||||||
"warning": "^3.0.0"
|
"warning": "^3.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prop-types": {
|
"loose-envify": {
|
||||||
"version": "15.6.2",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
"integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==",
|
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"loose-envify": "^1.3.1",
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
"object-assign": "^4.1.1"
|
}
|
||||||
|
},
|
||||||
|
"prop-types": {
|
||||||
|
"version": "15.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
|
||||||
|
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
|
||||||
|
"requires": {
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"react-is": "^16.8.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -702,7 +711,6 @@
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
|
||||||
"integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=",
|
"integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=",
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"kind-of": "^3.0.2",
|
"kind-of": "^3.0.2",
|
||||||
"longest": "^1.0.1",
|
"longest": "^1.0.1",
|
||||||
|
@ -898,11 +906,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apollo-errors": {
|
"apollo-errors": {
|
||||||
"version": "1.5.1",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/apollo-errors/-/apollo-errors-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/apollo-errors/-/apollo-errors-1.9.0.tgz",
|
||||||
"integrity": "sha512-gYAceMzNJfF+mUHH2/4UcZTkZtDY54arCTKGbKa7WU5IXnTJ4V+P94wHodcDkLLHWpHL8SW1hEgjN5ZINcPb1w==",
|
"integrity": "sha512-XVukHd0KLvgY6tNjsPS3/Re3U6RQlTKrTbIpqqeTMo2N34uQMr+H1UheV21o8hOZBAFosvBORVricJiP5vfmrw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"es6-error": "^4.0.0"
|
"assert": "^1.4.1",
|
||||||
|
"extendable-error": "^0.1.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apollo-link": {
|
"apollo-link": {
|
||||||
|
@ -1289,6 +1298,14 @@
|
||||||
"safer-buffer": "~2.1.0"
|
"safer-buffer": "~2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"assert": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz",
|
||||||
|
"integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=",
|
||||||
|
"requires": {
|
||||||
|
"util": "0.10.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"assert-err": {
|
"assert-err": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/assert-err/-/assert-err-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/assert-err/-/assert-err-1.1.0.tgz",
|
||||||
|
@ -2384,7 +2401,7 @@
|
||||||
},
|
},
|
||||||
"cheerio": {
|
"cheerio": {
|
||||||
"version": "0.22.0",
|
"version": "0.22.0",
|
||||||
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz",
|
"resolved": "http://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz",
|
||||||
"integrity": "sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=",
|
"integrity": "sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=",
|
||||||
"requires": {
|
"requires": {
|
||||||
"css-select": "~1.2.0",
|
"css-select": "~1.2.0",
|
||||||
|
@ -2831,7 +2848,7 @@
|
||||||
},
|
},
|
||||||
"css-select": {
|
"css-select": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
|
"resolved": "http://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
|
||||||
"integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=",
|
"integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=",
|
||||||
"requires": {
|
"requires": {
|
||||||
"boolbase": "~1.0.0",
|
"boolbase": "~1.0.0",
|
||||||
|
@ -3370,11 +3387,6 @@
|
||||||
"es6-symbol": "~3.1.1"
|
"es6-symbol": "~3.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"es6-error": {
|
|
||||||
"version": "4.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
|
|
||||||
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="
|
|
||||||
},
|
|
||||||
"es6-iterator": {
|
"es6-iterator": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
|
||||||
|
@ -3977,6 +3989,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
|
||||||
"integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ="
|
"integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ="
|
||||||
},
|
},
|
||||||
|
"extendable-error": {
|
||||||
|
"version": "0.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.5.tgz",
|
||||||
|
"integrity": "sha1-EiMIpwl7yJomOyxPvwiceBQOO20="
|
||||||
|
},
|
||||||
"extglob": {
|
"extglob": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz",
|
||||||
|
@ -6168,8 +6185,7 @@
|
||||||
"longest": {
|
"longest": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
|
||||||
"integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=",
|
"integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc="
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"loose-envify": {
|
"loose-envify": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
|
@ -6261,7 +6277,7 @@
|
||||||
"process": "^0.11.9",
|
"process": "^0.11.9",
|
||||||
"punycode": "^1.4.1",
|
"punycode": "^1.4.1",
|
||||||
"querystring-es3": "^0.2.1",
|
"querystring-es3": "^0.2.1",
|
||||||
"readable-stream": "git+https://github.com/meteor/readable-stream.git",
|
"readable-stream": "git+https://github.com/meteor/readable-stream.git#c688cdd193549919b840e8d72a86682d91961e12",
|
||||||
"stream-browserify": "^2.0.1",
|
"stream-browserify": "^2.0.1",
|
||||||
"string_decoder": "^1.0.1",
|
"string_decoder": "^1.0.1",
|
||||||
"timers-browserify": "^1.4.2",
|
"timers-browserify": "^1.4.2",
|
||||||
|
@ -6711,12 +6727,19 @@
|
||||||
"version": "git+https://github.com/meteor/readable-stream.git#c688cdd193549919b840e8d72a86682d91961e12",
|
"version": "git+https://github.com/meteor/readable-stream.git#c688cdd193549919b840e8d72a86682d91961e12",
|
||||||
"from": "git+https://github.com/meteor/readable-stream.git",
|
"from": "git+https://github.com/meteor/readable-stream.git",
|
||||||
"requires": {
|
"requires": {
|
||||||
"inherits": "~2.0.3",
|
"inherits": "~2.0.1",
|
||||||
"isarray": "~1.0.0",
|
"isarray": "~1.0.0",
|
||||||
"process-nextick-args": "~2.0.0",
|
"process-nextick-args": "~1.0.6",
|
||||||
"safe-buffer": "~5.1.1",
|
"safe-buffer": "^5.0.1",
|
||||||
"string_decoder": "~1.1.0",
|
"string_decoder": "~1.0.0",
|
||||||
"util-deprecate": "~1.0.1"
|
"util-deprecate": "~1.0.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"process-nextick-args": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
|
||||||
|
"integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M="
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rimraf": {
|
"rimraf": {
|
||||||
|
@ -7790,31 +7813,31 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-bootstrap": {
|
"react-bootstrap": {
|
||||||
"version": "1.0.0-beta.3",
|
"version": "1.0.0-beta.5",
|
||||||
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.0.0-beta.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.0.0-beta.5.tgz",
|
||||||
"integrity": "sha512-/eiSmRZE92q6m7uen3oAsOTGY4uBJkZDv32fwxUeyjesf834GUDaEhu1dzj10fmxSeVHW9O6UOKj9GkbwIIkMg==",
|
"integrity": "sha512-Osm0OtTbYwfsT1rpu88ESWuAHZxfaHFNKFiW8w3w+6YY9/bLEPHbGRZA6W21fg5yvcuKN9hJKT857TTHgY7SoQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/runtime": "^7.0.0",
|
"@babel/runtime": "^7.2.0",
|
||||||
"@react-bootstrap/react-popper": "1.2.1",
|
"@react-bootstrap/react-popper": "1.2.1",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"dom-helpers": "^3.2.0",
|
"dom-helpers": "^3.4.0",
|
||||||
"invariant": "^2.2.3",
|
"invariant": "^2.2.3",
|
||||||
"keycode": "^2.1.2",
|
"keycode": "^2.1.2",
|
||||||
"popper.js": "^1.14.3",
|
"popper.js": "^1.14.6",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"prop-types-extra": "^1.1.0",
|
"prop-types-extra": "^1.1.0",
|
||||||
"react-context-toolbox": "^1.2.3",
|
"react-context-toolbox": "^2.0.2",
|
||||||
"react-overlays": "^1.0.0-beta.17",
|
"react-overlays": "^1.0.0",
|
||||||
"react-prop-types": "^0.4.0",
|
"react-prop-types": "^0.4.0",
|
||||||
"react-transition-group": "^2.4.0",
|
"react-transition-group": "^2.5.1",
|
||||||
"uncontrollable": "^6.0.0",
|
"uncontrollable": "^6.0.0",
|
||||||
"warning": "^4.0.1"
|
"warning": "^4.0.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": {
|
"@babel/runtime": {
|
||||||
"version": "7.2.0",
|
"version": "7.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.4.tgz",
|
||||||
"integrity": "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg==",
|
"integrity": "sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"regenerator-runtime": "^0.12.0"
|
"regenerator-runtime": "^0.12.0"
|
||||||
}
|
}
|
||||||
|
@ -7824,6 +7847,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
|
||||||
"integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
|
"integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
|
||||||
},
|
},
|
||||||
|
"dom-helpers": {
|
||||||
|
"version": "3.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz",
|
||||||
|
"integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"invariant": {
|
"invariant": {
|
||||||
"version": "2.2.4",
|
"version": "2.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||||
|
@ -7833,12 +7864,23 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"prop-types": {
|
"prop-types": {
|
||||||
"version": "15.6.2",
|
"version": "15.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
|
||||||
"integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==",
|
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"loose-envify": "^1.3.1",
|
"loose-envify": "^1.4.0",
|
||||||
"object-assign": "^4.1.1"
|
"object-assign": "^4.1.1",
|
||||||
|
"react-is": "^16.8.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||||
|
"requires": {
|
||||||
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"regenerator-runtime": {
|
"regenerator-runtime": {
|
||||||
|
@ -7847,9 +7889,9 @@
|
||||||
"integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg=="
|
"integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg=="
|
||||||
},
|
},
|
||||||
"warning": {
|
"warning": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
|
||||||
"integrity": "sha512-wbTp09q/9C+jJn4KKJfJfoS6VleK/Dti0yqWSm6KMvJ4MRCXFQNapHuJXutJIrWV0Cf4AhTdeIe4qdKHR1+Hug==",
|
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"loose-envify": "^1.0.0"
|
"loose-envify": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
@ -7882,9 +7924,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-context-toolbox": {
|
"react-context-toolbox": {
|
||||||
"version": "1.2.3",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-context-toolbox/-/react-context-toolbox-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-context-toolbox/-/react-context-toolbox-2.0.2.tgz",
|
||||||
"integrity": "sha512-ArHw0UFDM6X8Z9lHZ1rZOhMcn8TXWC9y9sFpeJm11YTIlQsN4A0MadKcps2pCGMwWKmH0o/67t+TmVapwWm5Sw=="
|
"integrity": "sha512-tY4j0imkYC3n5ZlYSgFkaw7fmlCp3IoQQ6DxpqeNHzcD0hf+6V+/HeJxviLUZ1Rv1Yn3N3xyO2EhkkZwHn0m1A=="
|
||||||
},
|
},
|
||||||
"react-cookie": {
|
"react-cookie": {
|
||||||
"version": "2.1.6",
|
"version": "2.1.6",
|
||||||
|
@ -8001,9 +8043,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-is": {
|
"react-is": {
|
||||||
"version": "16.4.1",
|
"version": "16.8.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.4.tgz",
|
||||||
"integrity": "sha512-xpb0PpALlFWNw/q13A+1aHeyJyLYCg0/cCHPUA43zYluZuIPHaHL3k8OBsTgQtxqW0FhyDEMvi8fZ/+7+r4OSQ=="
|
"integrity": "sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA=="
|
||||||
},
|
},
|
||||||
"react-lifecycles-compat": {
|
"react-lifecycles-compat": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
|
@ -8038,9 +8080,9 @@
|
||||||
"integrity": "sha512-IBivBP7xayM7SbbVlAnKgHgoWdfCVqnNBNgQRY5x9iFQm55tFdolR02hX1fCJJtTEKnbaL1stB72/TZc6+p2+Q=="
|
"integrity": "sha512-IBivBP7xayM7SbbVlAnKgHgoWdfCVqnNBNgQRY5x9iFQm55tFdolR02hX1fCJJtTEKnbaL1stB72/TZc6+p2+Q=="
|
||||||
},
|
},
|
||||||
"react-overlays": {
|
"react-overlays": {
|
||||||
"version": "1.0.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-1.2.0.tgz",
|
||||||
"integrity": "sha512-YDuUwqWBuVvQvvxPTxKpCuaEZRegDhfgJdQUbVmWVI4gag53zukN/6tNxt0XZpgODQVzLf/w7dFuoDq7YMhygg==",
|
"integrity": "sha512-i/FCV8wR6aRaI+Kz/dpJhOdyx+ah2tN1RhT9InPrexyC4uzf3N4bNayFTGtUeQVacj57j1Mqh1CwV60/5153Iw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"dom-helpers": "^3.4.0",
|
"dom-helpers": "^3.4.0",
|
||||||
|
@ -8048,13 +8090,14 @@
|
||||||
"prop-types-extra": "^1.1.0",
|
"prop-types-extra": "^1.1.0",
|
||||||
"react-context-toolbox": "^2.0.2",
|
"react-context-toolbox": "^2.0.2",
|
||||||
"react-popper": "^1.3.2",
|
"react-popper": "^1.3.2",
|
||||||
|
"uncontrollable": "^6.0.0",
|
||||||
"warning": "^4.0.2"
|
"warning": "^4.0.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": {
|
"@babel/runtime": {
|
||||||
"version": "7.2.0",
|
"version": "7.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.4.tgz",
|
||||||
"integrity": "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg==",
|
"integrity": "sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"regenerator-runtime": "^0.12.0"
|
"regenerator-runtime": "^0.12.0"
|
||||||
}
|
}
|
||||||
|
@ -8072,19 +8115,23 @@
|
||||||
"@babel/runtime": "^7.1.2"
|
"@babel/runtime": "^7.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"prop-types": {
|
"loose-envify": {
|
||||||
"version": "15.6.2",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
"integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==",
|
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"loose-envify": "^1.3.1",
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
"object-assign": "^4.1.1"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-context-toolbox": {
|
"prop-types": {
|
||||||
"version": "2.0.2",
|
"version": "15.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-context-toolbox/-/react-context-toolbox-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
|
||||||
"integrity": "sha512-tY4j0imkYC3n5ZlYSgFkaw7fmlCp3IoQQ6DxpqeNHzcD0hf+6V+/HeJxviLUZ1Rv1Yn3N3xyO2EhkkZwHn0m1A=="
|
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
|
||||||
|
"requires": {
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"react-is": "^16.8.1"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"regenerator-runtime": {
|
"regenerator-runtime": {
|
||||||
"version": "0.12.1",
|
"version": "0.12.1",
|
||||||
|
@ -8092,9 +8139,9 @@
|
||||||
"integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg=="
|
"integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg=="
|
||||||
},
|
},
|
||||||
"warning": {
|
"warning": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
|
||||||
"integrity": "sha512-wbTp09q/9C+jJn4KKJfJfoS6VleK/Dti0yqWSm6KMvJ4MRCXFQNapHuJXutJIrWV0Cf4AhTdeIe4qdKHR1+Hug==",
|
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"loose-envify": "^1.0.0"
|
"loose-envify": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
@ -8111,9 +8158,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-popper": {
|
"react-popper": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.3.tgz",
|
||||||
"integrity": "sha512-UbFWj55Yt9uqvy0oZ+vULDL2Bw1oxeZF9/JzGyxQ5ypgauRH/XlarA5+HLZWro/Zss6Ht2kqpegtb6sYL8GUGw==",
|
"integrity": "sha512-ynMZBPkXONPc5K4P5yFWgZx5JGAUIP3pGGLNs58cfAPgK67olx7fmLp+AdpZ0+GoQ+ieFDa/z4cdV6u7sioH6w==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/runtime": "^7.1.2",
|
"@babel/runtime": "^7.1.2",
|
||||||
"create-react-context": "<=0.2.2",
|
"create-react-context": "<=0.2.2",
|
||||||
|
@ -8124,9 +8171,9 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": {
|
"@babel/runtime": {
|
||||||
"version": "7.2.0",
|
"version": "7.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.4.tgz",
|
||||||
"integrity": "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg==",
|
"integrity": "sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"regenerator-runtime": "^0.12.0"
|
"regenerator-runtime": "^0.12.0"
|
||||||
}
|
}
|
||||||
|
@ -8140,13 +8187,22 @@
|
||||||
"gud": "^1.0.0"
|
"gud": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"prop-types": {
|
"loose-envify": {
|
||||||
"version": "15.6.2",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
"integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==",
|
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"loose-envify": "^1.3.1",
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
"object-assign": "^4.1.1"
|
}
|
||||||
|
},
|
||||||
|
"prop-types": {
|
||||||
|
"version": "15.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
|
||||||
|
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
|
||||||
|
"requires": {
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"react-is": "^16.8.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"regenerator-runtime": {
|
"regenerator-runtime": {
|
||||||
|
@ -8160,9 +8216,9 @@
|
||||||
"integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q=="
|
"integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q=="
|
||||||
},
|
},
|
||||||
"warning": {
|
"warning": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
|
||||||
"integrity": "sha512-wbTp09q/9C+jJn4KKJfJfoS6VleK/Dti0yqWSm6KMvJ4MRCXFQNapHuJXutJIrWV0Cf4AhTdeIe4qdKHR1+Hug==",
|
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"loose-envify": "^1.0.0"
|
"loose-envify": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
@ -8202,27 +8258,6 @@
|
||||||
"prop-types": "^15.5.10"
|
"prop-types": "^15.5.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-router": {
|
|
||||||
"version": "3.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-3.2.0.tgz",
|
|
||||||
"integrity": "sha512-sXlLOg0TRCqnjCVskqBHGjzNjcJKUqXEKnDSuxMYJSPJNq9hROE9VsiIW2kfIq7Ev+20Iz0nxayekXyv0XNmsg==",
|
|
||||||
"requires": {
|
|
||||||
"create-react-class": "^15.5.1",
|
|
||||||
"history": "^3.0.0",
|
|
||||||
"hoist-non-react-statics": "^1.2.0",
|
|
||||||
"invariant": "^2.2.1",
|
|
||||||
"loose-envify": "^1.2.0",
|
|
||||||
"prop-types": "^15.5.6",
|
|
||||||
"warning": "^3.0.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"hoist-non-react-statics": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz",
|
|
||||||
"integrity": "sha1-qkSM8JhtVcxAdzsXF0t90GbLfPs="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"react-router-bootstrap": {
|
"react-router-bootstrap": {
|
||||||
"version": "0.24.4",
|
"version": "0.24.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-router-bootstrap/-/react-router-bootstrap-0.24.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-router-bootstrap/-/react-router-bootstrap-0.24.4.tgz",
|
||||||
|
@ -9569,9 +9604,9 @@
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"uncontrollable": {
|
"uncontrollable": {
|
||||||
"version": "6.0.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-6.1.0.tgz",
|
||||||
"integrity": "sha512-gmy2ESW40LGbijSbW5piBGiPv55IgyDbjQcMr7LkDR5icpw/06UgMqULAGDBAcFn2a9d/SRPgcb3oo8hdEUfIw==",
|
"integrity": "sha512-2TzEm0pLKauMBZfAZXsgQvLpZHEp95891frCZdGDrSG7dWYaIQhedwLAzi0X8pR8KHNqlmuYEb2cEgbQzr050A==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"invariant": "^2.2.4"
|
"invariant": "^2.2.4"
|
||||||
},
|
},
|
||||||
|
@ -9717,6 +9752,21 @@
|
||||||
"os-homedir": "^1.0.0"
|
"os-homedir": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"util": {
|
||||||
|
"version": "0.10.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
|
||||||
|
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
|
||||||
|
"requires": {
|
||||||
|
"inherits": "2.0.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
|
||||||
|
"integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"util-deprecate": {
|
"util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
@ -9852,7 +9902,7 @@
|
||||||
},
|
},
|
||||||
"chalk": {
|
"chalk": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
|
"resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
|
||||||
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
|
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
|
||||||
"requires": {
|
"requires": {
|
||||||
"ansi-styles": "^2.2.1",
|
"ansi-styles": "^2.2.1",
|
||||||
|
|
11
package.json
11
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "Vulcan",
|
"name": "Vulcan",
|
||||||
"version": "1.12.17",
|
"version": "1.13.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"npm": "^3.0"
|
"npm": "^3.0"
|
||||||
},
|
},
|
||||||
|
@ -21,7 +21,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.0.0-beta.55",
|
"@babel/runtime": "7.1.2",
|
||||||
"analytics-node": "^2.1.1",
|
"analytics-node": "^2.1.1",
|
||||||
"apollo-cache-inmemory": "^1.4.2",
|
"apollo-cache-inmemory": "^1.4.2",
|
||||||
"apollo-client": "2.4.12",
|
"apollo-client": "2.4.12",
|
||||||
|
@ -30,6 +30,7 @@
|
||||||
"apollo-link-error": "^1.1.5",
|
"apollo-link-error": "^1.1.5",
|
||||||
"apollo-link-schema": "^1.1.1",
|
"apollo-link-schema": "^1.1.1",
|
||||||
"apollo-link-state": "^0.4.2",
|
"apollo-link-state": "^0.4.2",
|
||||||
|
"apollo-link-watched-mutation": "^0.1.0",
|
||||||
"apollo-server": "2.3.3",
|
"apollo-server": "2.3.3",
|
||||||
"apollo-server-express": "2.3.3",
|
"apollo-server-express": "2.3.3",
|
||||||
"babel-runtime": "^6.26.0",
|
"babel-runtime": "^6.26.0",
|
||||||
|
@ -70,14 +71,14 @@
|
||||||
"moment": "^2.13.0",
|
"moment": "^2.13.0",
|
||||||
"prop-types": "^15.6.0",
|
"prop-types": "^15.6.0",
|
||||||
"qs": "^6.6.0",
|
"qs": "^6.6.0",
|
||||||
"react": "^16.2.0",
|
"react": "^16.4.1",
|
||||||
"react-addons-pure-render-mixin": "^15.4.1",
|
"react-addons-pure-render-mixin": "^15.4.1",
|
||||||
"react-apollo": "^2.4.1",
|
"react-apollo": "^2.4.1",
|
||||||
"react-bootstrap": "^1.0.0-beta.3",
|
"react-bootstrap": "^1.0.0-beta.5",
|
||||||
"react-bootstrap-datetimepicker": "0.0.22",
|
"react-bootstrap-datetimepicker": "0.0.22",
|
||||||
"react-cookie": "^2.1.4",
|
"react-cookie": "^2.1.4",
|
||||||
"react-datetime": "^2.11.1",
|
"react-datetime": "^2.11.1",
|
||||||
"react-dom": "^16.2.0",
|
"react-dom": "^16.4.1",
|
||||||
"react-dropzone": "^8.0.3",
|
"react-dropzone": "^8.0.3",
|
||||||
"react-helmet": "^5.1.3",
|
"react-helmet": "^5.1.3",
|
||||||
"react-intl": "^2.1.3",
|
"react-intl": "^2.1.3",
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { STATES } from '../../helpers.js';
|
||||||
|
|
||||||
class AccountsEnrollAccount extends PureComponent {
|
class AccountsEnrollAccount extends PureComponent {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const token = this.props.params.token;
|
const token = this.props.match.params.token;
|
||||||
Accounts._loginButtonsSession.set('enrollAccountToken', token);
|
Accounts._loginButtonsSession.set('enrollAccountToken', token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -265,9 +265,20 @@ export class AccountsLoginFormInner extends TrackerComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
fields() {
|
fields() {
|
||||||
const loginFields = [];
|
let loginFields = [];
|
||||||
const { formState } = this.state;
|
const { formState } = this.state;
|
||||||
|
|
||||||
|
// if extra fields have been specified, add onChange handler to them
|
||||||
|
if (this.props.extraFields) {
|
||||||
|
loginFields = this.props.extraFields.map(field => {
|
||||||
|
const { id } = field;
|
||||||
|
return {
|
||||||
|
...field,
|
||||||
|
onChange: this.handleChange.bind(this, id),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!hasPasswordService() && getLoginServices().length == 0) {
|
if (!hasPasswordService() && getLoginServices().length == 0) {
|
||||||
loginFields.push({
|
loginFields.push({
|
||||||
label: 'No login service added, i.e. accounts-password',
|
label: 'No login service added, i.e. accounts-password',
|
||||||
|
@ -766,6 +777,13 @@ export class AccountsLoginFormInner extends TrackerComponent {
|
||||||
onSubmitHook
|
onSubmitHook
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
|
// add extra fields to options
|
||||||
|
if (this.props.extraFields) {
|
||||||
|
this.props.extraFields.forEach(({ id })=> {
|
||||||
|
options[id] = this.state[id];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
let error = false;
|
let error = false;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'vulcan:accounts',
|
name: 'vulcan:accounts',
|
||||||
version: '1.12.17',
|
version: '1.13.0',
|
||||||
summary: 'Accounts UI for React in Meteor 1.3+',
|
summary: 'Accounts UI for React in Meteor 1.3+',
|
||||||
git: 'https://github.com/studiointeract/accounts-ui',
|
git: 'https://github.com/studiointeract/accounts-ui',
|
||||||
documentation: 'README.md',
|
documentation: 'README.md',
|
||||||
|
@ -9,7 +9,7 @@ Package.describe({
|
||||||
Package.onUse(function(api) {
|
Package.onUse(function(api) {
|
||||||
api.versionsFrom('1.6.1');
|
api.versionsFrom('1.6.1');
|
||||||
|
|
||||||
api.use('vulcan:core@1.12.17');
|
api.use('vulcan:core@1.13.0');
|
||||||
|
|
||||||
api.use('ecmascript');
|
api.use('ecmascript');
|
||||||
api.use('tracker');
|
api.use('tracker');
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'vulcan:admin',
|
name: 'vulcan:admin',
|
||||||
summary: 'Vulcan components package',
|
summary: 'Vulcan components package',
|
||||||
version: '1.12.17',
|
version: '1.13.0',
|
||||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ Package.onUse(function(api) {
|
||||||
'fourseven:scss@4.10.0',
|
'fourseven:scss@4.10.0',
|
||||||
'dynamic-import@0.1.1',
|
'dynamic-import@0.1.1',
|
||||||
// Vulcan packages
|
// Vulcan packages
|
||||||
'vulcan:core@1.12.17',
|
'vulcan:core@1.13.0',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
api.mainModule('lib/server/main.js', 'server');
|
api.mainModule('lib/server/main.js', 'server');
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'vulcan:cloudinary',
|
name: 'vulcan:cloudinary',
|
||||||
summary: 'Vulcan file upload package.',
|
summary: 'Vulcan file upload package.',
|
||||||
version: '1.12.17',
|
version: '1.13.0',
|
||||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||||
});
|
});
|
||||||
|
|
||||||
Package.onUse(function(api) {
|
Package.onUse(function(api) {
|
||||||
api.versionsFrom('1.6.1');
|
api.versionsFrom('1.6.1');
|
||||||
|
|
||||||
api.use(['vulcan:core@1.12.17']);
|
api.use(['vulcan:core@1.13.0']);
|
||||||
|
|
||||||
api.mainModule('lib/client/main.js', 'client');
|
api.mainModule('lib/client/main.js', 'client');
|
||||||
api.mainModule('lib/server/main.js', 'server');
|
api.mainModule('lib/server/main.js', 'server');
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Components, registerComponent } from 'meteor/vulcan:lib';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage, intlShape } from 'meteor/vulcan:i18n';
|
import { FormattedMessage, intlShape } from 'meteor/vulcan:i18n';
|
||||||
|
|
||||||
const EditButton = ({ style = 'primary', label, size, showId, modalProps, ...props }, { intl }) => (
|
const EditButton = ({ style = 'primary', label, size, showId, modalProps, formProps, ...props }, { intl }) => (
|
||||||
<Components.ModalTrigger
|
<Components.ModalTrigger
|
||||||
label={label || intl.formatMessage({ id: 'datatable.edit' })}
|
label={label || intl.formatMessage({ id: 'datatable.edit' })}
|
||||||
component={
|
component={
|
||||||
|
@ -12,7 +12,7 @@ const EditButton = ({ style = 'primary', label, size, showId, modalProps, ...pro
|
||||||
}
|
}
|
||||||
modalProps={modalProps}
|
modalProps={modalProps}
|
||||||
>
|
>
|
||||||
<Components.EditForm {...props} />
|
<Components.EditForm {...props} formProps={formProps}/>
|
||||||
</Components.ModalTrigger>
|
</Components.ModalTrigger>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ registerComponent('EditButton', EditButton);
|
||||||
EditForm Component
|
EditForm Component
|
||||||
|
|
||||||
*/
|
*/
|
||||||
const EditForm = ({ closeModal, successCallback, removeSuccessCallback, ...props }) => {
|
const EditForm = ({ closeModal, successCallback, removeSuccessCallback, formProps, ...props }) => {
|
||||||
|
|
||||||
const success = successCallback
|
const success = successCallback
|
||||||
? document => {
|
? document => {
|
||||||
|
@ -46,7 +46,7 @@ const EditForm = ({ closeModal, successCallback, removeSuccessCallback, ...props
|
||||||
: closeModal;
|
: closeModal;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Components.SmartForm successCallback={success} removeSuccessCallback={remove} {...props} />
|
<Components.SmartForm successCallback={success} removeSuccessCallback={remove} {...formProps} {...props} />
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
registerComponent('EditForm', EditForm);
|
registerComponent('EditForm', EditForm);
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Components, registerComponent } from 'meteor/vulcan:lib';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage, intlShape } from 'meteor/vulcan:i18n';
|
import { FormattedMessage, intlShape } from 'meteor/vulcan:i18n';
|
||||||
|
|
||||||
const NewButton = ({ collection, size, label, style = 'primary', ...props }, { intl }) => (
|
const NewButton = ({ collection, size, label, style = 'primary', formProps, ...props }, { intl }) => (
|
||||||
<Components.ModalTrigger
|
<Components.ModalTrigger
|
||||||
label={label || intl.formatMessage({ id: 'datatable.new' })}
|
label={label || intl.formatMessage({ id: 'datatable.new' })}
|
||||||
component={
|
component={
|
||||||
|
@ -11,7 +11,7 @@ const NewButton = ({ collection, size, label, style = 'primary', ...props }, { i
|
||||||
</Components.Button>
|
</Components.Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Components.NewForm collection={collection} {...props} />
|
<Components.NewForm collection={collection} formProps={formProps} {...props} />
|
||||||
</Components.ModalTrigger>
|
</Components.ModalTrigger>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ registerComponent('NewButton', NewButton);
|
||||||
NewForm Component
|
NewForm Component
|
||||||
|
|
||||||
*/
|
*/
|
||||||
const NewForm = ({ closeModal, successCallback, ...props }) => {
|
const NewForm = ({ closeModal, successCallback, formProps, ...props }) => {
|
||||||
|
|
||||||
const success = successCallback
|
const success = successCallback
|
||||||
? document => {
|
? document => {
|
||||||
|
@ -37,6 +37,6 @@ const NewForm = ({ closeModal, successCallback, ...props }) => {
|
||||||
}
|
}
|
||||||
: closeModal;
|
: closeModal;
|
||||||
|
|
||||||
return <Components.SmartForm successCallback={success} {...props} />;
|
return <Components.SmartForm successCallback={success} {...formProps} {...props} />;
|
||||||
};
|
};
|
||||||
registerComponent('NewForm', NewForm);
|
registerComponent('NewForm', NewForm);
|
||||||
|
|
|
@ -28,6 +28,135 @@ import get from 'lodash/get';
|
||||||
|
|
||||||
const defaultOptions = { create: true, update: true, upsert: true, delete: true };
|
const defaultOptions = { create: true, update: true, upsert: true, delete: true };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safe getter
|
||||||
|
* Must returns null if the document is absent (eg in case of validation failure)
|
||||||
|
* @param {*} mutation
|
||||||
|
* @param {*} mutationName
|
||||||
|
*/
|
||||||
|
const getDocumentFromMutation = (mutation, mutationName) => {
|
||||||
|
const mutationData = (mutation.result.data[mutationName] || {});
|
||||||
|
const document = mutationData.data;
|
||||||
|
return document;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCreateMutationName = (typeName) => `create${typeName}`;
|
||||||
|
const getUpdateMutationName = (typeName) => `update${typeName}`;
|
||||||
|
const getDeleteMutationName = (typeName) => `delete${typeName}`;
|
||||||
|
const getUpsertMutationName = (typeName) => `upsert${typeName}`;
|
||||||
|
const getMultiResolverName = (typeName) => Utils.camelCaseify(Utils.pluralize(typeName));
|
||||||
|
const getMultiQueryName = (typeName) => `multi${typeName}Query`;
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Handle post-mutation updates of the client cache
|
||||||
|
TODO: this is a client only function
|
||||||
|
it should be called by a callback on collection create?
|
||||||
|
*/
|
||||||
|
export const registerWatchedMutations = (mutations, typeName) => {
|
||||||
|
if (Meteor.isClient) {
|
||||||
|
const multiQueryName = getMultiQueryName(typeName);
|
||||||
|
const multiResolverName = getMultiResolverName(typeName);
|
||||||
|
// create
|
||||||
|
if (mutations.create) {
|
||||||
|
const mutationName = mutations.create.name;
|
||||||
|
registerWatchedMutation(mutationName, multiQueryName, ({ mutation, query }) => {
|
||||||
|
// get mongo selector and options objects based on current terms
|
||||||
|
const terms = query.variables.input.terms;
|
||||||
|
const collection = Collections.find(c => c.typeName === typeName);
|
||||||
|
const parameters = collection.getParameters(terms /* apolloClient */);
|
||||||
|
const { selector, options } = parameters;
|
||||||
|
let results = query.result;
|
||||||
|
const document = getDocumentFromMutation(mutation, mutationName);
|
||||||
|
// nothing to add
|
||||||
|
if (!document) return results;
|
||||||
|
|
||||||
|
if (belongsToSet(document, selector)) {
|
||||||
|
if (!isInSet(results[multiResolverName], document)) {
|
||||||
|
// make sure document hasn't been already added as this may be called several times
|
||||||
|
results[multiResolverName] = addToSet(results[multiResolverName], document);
|
||||||
|
}
|
||||||
|
results[multiResolverName] = reorderSet(results[multiResolverName], options.sort);
|
||||||
|
}
|
||||||
|
|
||||||
|
results[multiResolverName].__typename = `Multi${typeName}Output`;
|
||||||
|
|
||||||
|
// console.log('// create');
|
||||||
|
// console.log(mutation);
|
||||||
|
// console.log(query);
|
||||||
|
// console.log(collection);
|
||||||
|
// console.log(parameters);
|
||||||
|
// console.log(results);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//update
|
||||||
|
if (mutations.update) {
|
||||||
|
const mutationName = mutations.update.name;
|
||||||
|
registerWatchedMutation(mutationName, multiQueryName, ({ mutation, query }) => {
|
||||||
|
// get mongo selector and options objects based on current terms
|
||||||
|
const terms = query.variables.input.terms;
|
||||||
|
const collection = Collections.find(c => c.typeName === typeName);
|
||||||
|
const parameters = collection.getParameters(terms /* apolloClient */);
|
||||||
|
const { selector, options } = parameters;
|
||||||
|
let results = query.result;
|
||||||
|
const document = getDocumentFromMutation(mutation, mutationName);
|
||||||
|
// nothing to update
|
||||||
|
if (!document) return results;
|
||||||
|
|
||||||
|
if (belongsToSet(document, selector)) {
|
||||||
|
// edited document belongs to the list
|
||||||
|
if (!isInSet(results[multiResolverName], document)) {
|
||||||
|
// if document wasn't already in list, add it
|
||||||
|
results[multiResolverName] = addToSet(results[multiResolverName], document);
|
||||||
|
} else {
|
||||||
|
// if document was already in the list, update it
|
||||||
|
results[multiResolverName] = updateInSet(results[multiResolverName], document);
|
||||||
|
}
|
||||||
|
results[multiResolverName] = reorderSet(
|
||||||
|
results[multiResolverName],
|
||||||
|
options.sort,
|
||||||
|
selector
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// if edited doesn't belong to current list anymore (based on view selector), remove it
|
||||||
|
results[multiResolverName] = removeFromSet(results[multiResolverName], document);
|
||||||
|
}
|
||||||
|
|
||||||
|
results[multiResolverName].__typename = `Multi${typeName}Output`;
|
||||||
|
|
||||||
|
// console.log('// update');
|
||||||
|
// console.log(mutation);
|
||||||
|
// console.log(query);
|
||||||
|
// console.log(parameters);
|
||||||
|
// console.log(results);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//delete
|
||||||
|
if (mutations.delete) {
|
||||||
|
const mutationName = mutations.delete.name;
|
||||||
|
registerWatchedMutation(mutationName, multiQueryName, ({ mutation, query }) => {
|
||||||
|
let results = query.result;
|
||||||
|
const document = getDocumentFromMutation(mutation, mutationName);
|
||||||
|
// nothing to delete
|
||||||
|
if (!document) return results;
|
||||||
|
results[multiResolverName] = removeFromSet(results[multiResolverName], document);
|
||||||
|
results[multiResolverName].__typename = `Multi${typeName}Output`;
|
||||||
|
// console.log('// delete')
|
||||||
|
// console.log(mutation);
|
||||||
|
// console.log(query);
|
||||||
|
// console.log(parameters);
|
||||||
|
// console.log(results);
|
||||||
|
return results;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export function getDefaultMutations(options) {
|
export function getDefaultMutations(options) {
|
||||||
let typeName, collectionName, mutationOptions;
|
let typeName, collectionName, mutationOptions;
|
||||||
|
|
||||||
|
@ -46,17 +175,16 @@ export function getDefaultMutations(options) {
|
||||||
// register callbacks for documentation purposes
|
// register callbacks for documentation purposes
|
||||||
registerCollectionCallbacks(typeName, mutationOptions);
|
registerCollectionCallbacks(typeName, mutationOptions);
|
||||||
|
|
||||||
const multiResolverName = Utils.camelCaseify(Utils.pluralize(typeName));
|
|
||||||
const multiQueryName = `multi${typeName}Query`;
|
|
||||||
const mutations = {};
|
const mutations = {};
|
||||||
|
|
||||||
if (mutationOptions.create) {
|
if (mutationOptions.create) {
|
||||||
// mutation for inserting a new document
|
// mutation for inserting a new document
|
||||||
|
|
||||||
const mutationName = `create${typeName}`;
|
const mutationName = getCreateMutationName(typeName);
|
||||||
|
|
||||||
const createMutation = {
|
const createMutation = {
|
||||||
description: `Mutation for creating new ${typeName} documents`,
|
description: `Mutation for creating new ${typeName} documents`,
|
||||||
|
name: mutationName,
|
||||||
|
|
||||||
// check function called on a user to see if they can perform the operation
|
// check function called on a user to see if they can perform the operation
|
||||||
check(user, document) {
|
check(user, document) {
|
||||||
|
@ -99,51 +227,16 @@ export function getDefaultMutations(options) {
|
||||||
mutations.create = createMutation;
|
mutations.create = createMutation;
|
||||||
// OpenCRUD backwards compatibility
|
// OpenCRUD backwards compatibility
|
||||||
mutations.new = createMutation;
|
mutations.new = createMutation;
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Handle post-mutation updates of the client cache
|
|
||||||
|
|
||||||
*/
|
|
||||||
if (Meteor.isClient) {
|
|
||||||
registerWatchedMutation(mutationName, multiQueryName, ({ mutation, query }) => {
|
|
||||||
// get mongo selector and options objects based on current terms
|
|
||||||
const terms = query.variables.input.terms;
|
|
||||||
const collection = Collections.find(c => c.typeName === typeName);
|
|
||||||
const parameters = collection.getParameters(terms /* apolloClient */);
|
|
||||||
const { selector, options } = parameters;
|
|
||||||
let results = query.result;
|
|
||||||
const document = get(mutation, `result.data['${mutationName}'.data]`, {});
|
|
||||||
|
|
||||||
if (belongsToSet(document, selector)) {
|
|
||||||
if (!isInSet(results[multiResolverName], document)) {
|
|
||||||
// make sure document hasn't been already added as this may be called several times
|
|
||||||
results[multiResolverName] = addToSet(results[multiResolverName], document);
|
|
||||||
}
|
|
||||||
results[multiResolverName] = reorderSet(results[multiResolverName], options.sort);
|
|
||||||
}
|
|
||||||
|
|
||||||
results[multiResolverName].__typename = `Multi${typeName}Output`;
|
|
||||||
|
|
||||||
// console.log('// create');
|
|
||||||
// console.log(mutation);
|
|
||||||
// console.log(query);
|
|
||||||
// console.log(collection);
|
|
||||||
// console.log(parameters);
|
|
||||||
// console.log(results);
|
|
||||||
|
|
||||||
return results;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mutationOptions.update) {
|
if (mutationOptions.update) {
|
||||||
// mutation for editing a specific document
|
// mutation for editing a specific document
|
||||||
|
|
||||||
const mutationName = `update${typeName}`;
|
const mutationName = getUpdateMutationName(typeName);
|
||||||
|
|
||||||
const updateMutation = {
|
const updateMutation = {
|
||||||
description: `Mutation for updating a ${typeName} document`,
|
description: `Mutation for updating a ${typeName} document`,
|
||||||
|
name: mutationName,
|
||||||
|
|
||||||
// check function called on a user and document to see if they can perform the operation
|
// check function called on a user and document to see if they can perform the operation
|
||||||
check(user, document) {
|
check(user, document) {
|
||||||
|
@ -160,13 +253,13 @@ export function getDefaultMutations(options) {
|
||||||
// OpenCRUD backwards compatibility
|
// OpenCRUD backwards compatibility
|
||||||
return Users.owns(user, document)
|
return Users.owns(user, document)
|
||||||
? Users.canDo(user, [
|
? Users.canDo(user, [
|
||||||
`${typeName.toLowerCase()}.update.own`,
|
`${typeName.toLowerCase()}.update.own`,
|
||||||
`${collectionName.toLowerCase()}.edit.own`,
|
`${collectionName.toLowerCase()}.edit.own`,
|
||||||
])
|
])
|
||||||
: Users.canDo(user, [
|
: Users.canDo(user, [
|
||||||
`${typeName.toLowerCase()}.update.all`,
|
`${typeName.toLowerCase()}.update.all`,
|
||||||
`${collectionName.toLowerCase()}.edit.all`,
|
`${collectionName.toLowerCase()}.edit.all`,
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
async mutation(root, { selector, data }, context) {
|
async mutation(root, { selector, data }, context) {
|
||||||
|
@ -212,56 +305,13 @@ export function getDefaultMutations(options) {
|
||||||
// OpenCRUD backwards compatibility
|
// OpenCRUD backwards compatibility
|
||||||
mutations.edit = updateMutation;
|
mutations.edit = updateMutation;
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Handle post-mutation updates of the client cache
|
|
||||||
|
|
||||||
*/
|
|
||||||
if (Meteor.isClient) {
|
|
||||||
registerWatchedMutation(mutationName, multiQueryName, ({ mutation, query }) => {
|
|
||||||
// get mongo selector and options objects based on current terms
|
|
||||||
const terms = query.variables.input.terms;
|
|
||||||
const collection = Collections.find(c => c.typeName === typeName);
|
|
||||||
const parameters = collection.getParameters(terms /* apolloClient */);
|
|
||||||
const { selector, options } = parameters;
|
|
||||||
let results = query.result;
|
|
||||||
const document = get(mutation, `result.data['${mutationName}'.data]`, {});
|
|
||||||
|
|
||||||
if (belongsToSet(document, selector)) {
|
|
||||||
// edited document belongs to the list
|
|
||||||
if (!isInSet(results[multiResolverName], document)) {
|
|
||||||
// if document wasn't already in list, add it
|
|
||||||
results[multiResolverName] = addToSet(results[multiResolverName], document);
|
|
||||||
} else {
|
|
||||||
// if document was already in the list, update it
|
|
||||||
results[multiResolverName] = updateInSet(results[multiResolverName], document);
|
|
||||||
}
|
|
||||||
results[multiResolverName] = reorderSet(
|
|
||||||
results[multiResolverName],
|
|
||||||
options.sort,
|
|
||||||
selector
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// if edited doesn't belong to current list anymore (based on view selector), remove it
|
|
||||||
results[multiResolverName] = removeFromSet(results[multiResolverName], document);
|
|
||||||
}
|
|
||||||
|
|
||||||
results[multiResolverName].__typename = `Multi${typeName}Output`;
|
|
||||||
|
|
||||||
// console.log('// update');
|
|
||||||
// console.log(mutation);
|
|
||||||
// console.log(query);
|
|
||||||
// console.log(parameters);
|
|
||||||
// console.log(results);
|
|
||||||
|
|
||||||
return results;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (mutationOptions.upsert) {
|
if (mutationOptions.upsert) {
|
||||||
// mutation for upserting a specific document
|
// mutation for upserting a specific document
|
||||||
|
const mutationName = getUpsertMutationName(typeName);
|
||||||
mutations.upsert = {
|
mutations.upsert = {
|
||||||
description: `Mutation for upserting a ${typeName} document`,
|
description: `Mutation for upserting a ${typeName} document`,
|
||||||
|
name: mutationName,
|
||||||
|
|
||||||
async mutation(root, { selector, data }, context) {
|
async mutation(root, { selector, data }, context) {
|
||||||
const collection = context[collectionName];
|
const collection = context[collectionName];
|
||||||
|
@ -286,10 +336,11 @@ export function getDefaultMutations(options) {
|
||||||
if (mutationOptions.delete) {
|
if (mutationOptions.delete) {
|
||||||
// mutation for removing a specific document (same checks as edit mutation)
|
// mutation for removing a specific document (same checks as edit mutation)
|
||||||
|
|
||||||
const mutationName = `delete${typeName}`;
|
const mutationName = getDeleteMutationName(typeName);
|
||||||
|
|
||||||
const deleteMutation = {
|
const deleteMutation = {
|
||||||
description: `Mutation for deleting a ${typeName} document`,
|
description: `Mutation for deleting a ${typeName} document`,
|
||||||
|
name: mutationName,
|
||||||
|
|
||||||
check(user, document) {
|
check(user, document) {
|
||||||
// OpenCRUD backwards compatibility
|
// OpenCRUD backwards compatibility
|
||||||
|
@ -302,13 +353,13 @@ export function getDefaultMutations(options) {
|
||||||
// OpenCRUD backwards compatibility
|
// OpenCRUD backwards compatibility
|
||||||
return Users.owns(user, document)
|
return Users.owns(user, document)
|
||||||
? Users.canDo(user, [
|
? Users.canDo(user, [
|
||||||
`${typeName.toLowerCase()}.delete.own`,
|
`${typeName.toLowerCase()}.delete.own`,
|
||||||
`${collectionName.toLowerCase()}.remove.own`,
|
`${collectionName.toLowerCase()}.remove.own`,
|
||||||
])
|
])
|
||||||
: Users.canDo(user, [
|
: Users.canDo(user, [
|
||||||
`${typeName.toLowerCase()}.delete.all`,
|
`${typeName.toLowerCase()}.delete.all`,
|
||||||
`${collectionName.toLowerCase()}.remove.all`,
|
`${collectionName.toLowerCase()}.remove.all`,
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
async mutation(root, { selector }, context) {
|
async mutation(root, { selector }, context) {
|
||||||
|
@ -351,26 +402,10 @@ export function getDefaultMutations(options) {
|
||||||
// OpenCRUD backwards compatibility
|
// OpenCRUD backwards compatibility
|
||||||
mutations.remove = deleteMutation;
|
mutations.remove = deleteMutation;
|
||||||
|
|
||||||
/*
|
}
|
||||||
|
|
||||||
Handle post-mutation updates of the client cache
|
if (Meteor.isClient) {
|
||||||
|
registerWatchedMutations(mutations, typeName);
|
||||||
*/
|
|
||||||
if (Meteor.isClient) {
|
|
||||||
registerWatchedMutation(mutationName, multiQueryName, ({ mutation, query }) => {
|
|
||||||
let results = query.result;
|
|
||||||
const document = get(mutation, `result.data['${mutationName}'.data]`, {});
|
|
||||||
|
|
||||||
results[multiResolverName] = removeFromSet(results[multiResolverName], document);
|
|
||||||
results[multiResolverName].__typename = `Multi${typeName}Output`;
|
|
||||||
// console.log('// delete')
|
|
||||||
// console.log(mutation);
|
|
||||||
// console.log(query);
|
|
||||||
// console.log(parameters);
|
|
||||||
// console.log(results);
|
|
||||||
return results;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return mutations;
|
return mutations;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'vulcan:core',
|
name: 'vulcan:core',
|
||||||
summary: 'Vulcan core package',
|
summary: 'Vulcan core package',
|
||||||
version: '1.12.17',
|
version: '1.13.0',
|
||||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -9,14 +9,14 @@ Package.onUse(function(api) {
|
||||||
api.versionsFrom('1.6.1');
|
api.versionsFrom('1.6.1');
|
||||||
|
|
||||||
api.use([
|
api.use([
|
||||||
'vulcan:lib@1.12.17',
|
'vulcan:lib@1.13.0',
|
||||||
'vulcan:i18n@1.12.17',
|
'vulcan:i18n@1.13.0',
|
||||||
'vulcan:users@1.12.17',
|
'vulcan:users@1.13.0',
|
||||||
'vulcan:routing@1.12.17',
|
'vulcan:routing@1.13.0',
|
||||||
'vulcan:debug@1.12.17',
|
'vulcan:debug@1.13.0',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
api.imply(['vulcan:lib@1.12.17']);
|
api.imply(['vulcan:lib@1.13.0']);
|
||||||
|
|
||||||
api.mainModule('lib/server/main.js', 'server');
|
api.mainModule('lib/server/main.js', 'server');
|
||||||
api.mainModule('lib/client/main.js', 'client');
|
api.mainModule('lib/client/main.js', 'client');
|
||||||
|
@ -25,4 +25,5 @@ Package.onUse(function(api) {
|
||||||
Package.onTest(function(api) {
|
Package.onTest(function(api) {
|
||||||
api.use(['ecmascript', 'meteortesting:mocha', 'vulcan:test', 'vulcan:core']);
|
api.use(['ecmascript', 'meteortesting:mocha', 'vulcan:test', 'vulcan:core']);
|
||||||
api.mainModule('./test/index.js');
|
api.mainModule('./test/index.js');
|
||||||
|
api.mainModule('./test/client/index.js', ['client']);
|
||||||
});
|
});
|
||||||
|
|
1
packages/vulcan-core/test/client/index.js
Normal file
1
packages/vulcan-core/test/client/index.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
import './mutations.test';
|
19
packages/vulcan-core/test/client/mutations.test.js
Normal file
19
packages/vulcan-core/test/client/mutations.test.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { registerWatchedMutations } from '../../lib/modules/default_mutations';
|
||||||
|
import expect from 'expect';
|
||||||
|
|
||||||
|
describe('vulcan:core/registerWatchedMutations', function(){
|
||||||
|
it('should registerWatchedMutations without failing', function(){
|
||||||
|
registerWatchedMutations({
|
||||||
|
create:{
|
||||||
|
name:'createFoo'
|
||||||
|
},
|
||||||
|
update:{
|
||||||
|
name:'updateFoo'
|
||||||
|
},
|
||||||
|
delete:{
|
||||||
|
name: 'deleteFoo'
|
||||||
|
}
|
||||||
|
}, 'Foo');
|
||||||
|
expect(true).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,3 +1,4 @@
|
||||||
|
import './mutations.test';
|
||||||
import './resolvers.test';
|
import './resolvers.test';
|
||||||
import './components.test';
|
import './components.test';
|
||||||
import './containers.test';
|
import './containers.test';
|
||||||
|
|
29
packages/vulcan-core/test/mutations.test.js
Normal file
29
packages/vulcan-core/test/mutations.test.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { getDefaultMutations } from '../lib/modules/default_mutations';
|
||||||
|
|
||||||
|
import expect from 'expect';
|
||||||
|
|
||||||
|
|
||||||
|
describe('vulcan:core/default_mutations', function() {
|
||||||
|
|
||||||
|
it('returns mutations', function(){
|
||||||
|
const mutations = getDefaultMutations({
|
||||||
|
typeName:'Foo',
|
||||||
|
collectionName:'Foos',
|
||||||
|
options: {}
|
||||||
|
});
|
||||||
|
expect(mutations.create).toBeDefined();
|
||||||
|
expect(mutations.update).toBeDefined();
|
||||||
|
expect(mutations.delete).toBeDefined();
|
||||||
|
});
|
||||||
|
it('preserves openCRUD backward compatibility', function(){
|
||||||
|
const mutations = getDefaultMutations({
|
||||||
|
typeName:'Foo',
|
||||||
|
collectionName:'Foos',
|
||||||
|
options: {}
|
||||||
|
});
|
||||||
|
expect(mutations.new).toBeDefined();
|
||||||
|
expect(mutations.edit).toBeDefined();
|
||||||
|
expect(mutations.remove).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
|
@ -1,7 +1,7 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'vulcan:debug',
|
name: 'vulcan:debug',
|
||||||
summary: 'Vulcan debug package',
|
summary: 'Vulcan debug package',
|
||||||
version: '1.12.17',
|
version: '1.13.0',
|
||||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||||
debugOnly: true,
|
debugOnly: true,
|
||||||
});
|
});
|
||||||
|
@ -15,8 +15,8 @@ Package.onUse(function(api) {
|
||||||
|
|
||||||
// Vulcan packages
|
// Vulcan packages
|
||||||
|
|
||||||
'vulcan:lib@1.12.17',
|
'vulcan:lib@1.13.0',
|
||||||
'vulcan:email@1.12.17',
|
'vulcan:email@1.13.0',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
api.addFiles(['lib/stylesheets/debug.scss'], ['client']);
|
api.addFiles(['lib/stylesheets/debug.scss'], ['client']);
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'vulcan:email',
|
name: 'vulcan:email',
|
||||||
summary: 'Vulcan email package',
|
summary: 'Vulcan email package',
|
||||||
version: '1.12.17',
|
version: '1.13.0',
|
||||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||||
});
|
});
|
||||||
|
|
||||||
Package.onUse(function(api) {
|
Package.onUse(function(api) {
|
||||||
api.versionsFrom('1.6.1');
|
api.versionsFrom('1.6.1');
|
||||||
|
|
||||||
api.use(['vulcan:lib@1.12.17']);
|
api.use(['vulcan:lib@1.13.0']);
|
||||||
|
|
||||||
api.mainModule('lib/server.js', 'server');
|
api.mainModule('lib/server.js', 'server');
|
||||||
api.mainModule('lib/client.js', 'client');
|
api.mainModule('lib/client.js', 'client');
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'vulcan:embed',
|
name: 'vulcan:embed',
|
||||||
summary: 'Vulcan Embed package',
|
summary: 'Vulcan Embed package',
|
||||||
version: '1.12.17',
|
version: '1.13.0',
|
||||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||||
});
|
});
|
||||||
|
|
||||||
Package.onUse(function(api) {
|
Package.onUse(function(api) {
|
||||||
api.versionsFrom('1.6.1');
|
api.versionsFrom('1.6.1');
|
||||||
|
|
||||||
api.use(['http', 'vulcan:core@1.12.17', 'fourseven:scss@4.10.0']);
|
api.use(['http', 'vulcan:core@1.13.0', 'fourseven:scss@4.10.0']);
|
||||||
|
|
||||||
api.addFiles(['lib/stylesheets/embedly.scss'], ['client']);
|
api.addFiles(['lib/stylesheets/embedly.scss'], ['client']);
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'vulcan:errors-sentry',
|
name: 'vulcan:errors-sentry',
|
||||||
summary: 'Vulcan Sentry error tracking package',
|
summary: 'Vulcan Sentry error tracking package',
|
||||||
version: '1.12.17',
|
version: '1.13.0',
|
||||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||||
});
|
});
|
||||||
|
|
||||||
Package.onUse(function(api) {
|
Package.onUse(function(api) {
|
||||||
api.versionsFrom('1.6.1');
|
api.versionsFrom('1.6.1');
|
||||||
|
|
||||||
api.use(['ecmascript', 'vulcan:core@1.12.17', 'vulcan:users@1.12.17', 'vulcan:errors@1.12.17']);
|
api.use(['ecmascript', 'vulcan:core@1.13.0', 'vulcan:users@1.13.0', 'vulcan:errors@1.13.0']);
|
||||||
|
|
||||||
api.mainModule('lib/server/main.js', 'server');
|
api.mainModule('lib/server/main.js', 'server');
|
||||||
api.mainModule('lib/client/main.js', 'client');
|
api.mainModule('lib/client/main.js', 'client');
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'vulcan:errors',
|
name: 'vulcan:errors',
|
||||||
summary: 'Vulcan error tracking package',
|
summary: 'Vulcan error tracking package',
|
||||||
version: '1.12.17',
|
version: '1.13.0',
|
||||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||||
});
|
});
|
||||||
|
|
||||||
Package.onUse(function(api) {
|
Package.onUse(function(api) {
|
||||||
api.versionsFrom('1.6.1');
|
api.versionsFrom('1.6.1');
|
||||||
|
|
||||||
api.use(['ecmascript', 'vulcan:core@1.12.17']);
|
api.use(['ecmascript', 'vulcan:core@1.13.0']);
|
||||||
|
|
||||||
api.mainModule('lib/server/main.js', 'server');
|
api.mainModule('lib/server/main.js', 'server');
|
||||||
api.mainModule('lib/client/main.js', 'client');
|
api.mainModule('lib/client/main.js', 'client');
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'vulcan:events-ga',
|
name: 'vulcan:events-ga',
|
||||||
summary: 'Vulcan Google Analytics event tracking package',
|
summary: 'Vulcan Google Analytics event tracking package',
|
||||||
version: '1.12.17',
|
version: '1.13.0',
|
||||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||||
});
|
});
|
||||||
|
|
||||||
Package.onUse(function(api) {
|
Package.onUse(function(api) {
|
||||||
api.versionsFrom('1.6.1');
|
api.versionsFrom('1.6.1');
|
||||||
|
|
||||||
api.use(['vulcan:core@1.12.17', 'vulcan:events@1.12.17']);
|
api.use(['vulcan:core@1.13.0', 'vulcan:events@1.13.0']);
|
||||||
|
|
||||||
api.mainModule('lib/server/main.js', 'server');
|
api.mainModule('lib/server/main.js', 'server');
|
||||||
api.mainModule('lib/client/main.js', 'client');
|
api.mainModule('lib/client/main.js', 'client');
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'vulcan:events-intercom',
|
name: 'vulcan:events-intercom',
|
||||||
summary: 'Vulcan Intercom integration package.',
|
summary: 'Vulcan Intercom integration package.',
|
||||||
version: '1.12.17',
|
version: '1.13.0',
|
||||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||||
});
|
});
|
||||||
|
|
||||||
Package.onUse(function(api) {
|
Package.onUse(function(api) {
|
||||||
api.versionsFrom('1.6.1');
|
api.versionsFrom('1.6.1');
|
||||||
|
|
||||||
api.use(['vulcan:core@1.12.17', 'vulcan:events@1.12.17']);
|
api.use(['vulcan:core@1.13.0', 'vulcan:events@1.13.0']);
|
||||||
|
|
||||||
api.mainModule('lib/client/main.js', 'client');
|
api.mainModule('lib/client/main.js', 'client');
|
||||||
api.mainModule('lib/server/main.js', 'server');
|
api.mainModule('lib/server/main.js', 'server');
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'vulcan:events-internal',
|
name: 'vulcan:events-internal',
|
||||||
summary: 'Vulcan internal event tracking package',
|
summary: 'Vulcan internal event tracking package',
|
||||||
version: '1.12.17',
|
version: '1.13.0',
|
||||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||||
});
|
});
|
||||||
|
|
||||||
Package.onUse(function(api) {
|
Package.onUse(function(api) {
|
||||||
api.versionsFrom('1.6.1');
|
api.versionsFrom('1.6.1');
|
||||||
|
|
||||||
api.use(['vulcan:core@1.12.17', 'vulcan:events@1.12.17']);
|
api.use(['vulcan:core@1.13.0', 'vulcan:events@1.13.0']);
|
||||||
|
|
||||||
api.mainModule('lib/server/main.js', 'server');
|
api.mainModule('lib/server/main.js', 'server');
|
||||||
api.mainModule('lib/client/main.js', 'client');
|
api.mainModule('lib/client/main.js', 'client');
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'vulcan:events-segment',
|
name: 'vulcan:events-segment',
|
||||||
summary: 'Vulcan Segment',
|
summary: 'Vulcan Segment',
|
||||||
version: '1.12.17',
|
version: '1.13.0',
|
||||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||||
});
|
});
|
||||||
|
|
||||||
Package.onUse(function(api) {
|
Package.onUse(function(api) {
|
||||||
api.versionsFrom('1.6.1');
|
api.versionsFrom('1.6.1');
|
||||||
|
|
||||||
api.use(['vulcan:core@1.12.17', 'vulcan:events@1.12.17']);
|
api.use(['vulcan:core@1.13.0', 'vulcan:events@1.13.0']);
|
||||||
|
|
||||||
api.mainModule('lib/server/main.js', 'server');
|
api.mainModule('lib/server/main.js', 'server');
|
||||||
api.mainModule('lib/client/main.js', 'client');
|
api.mainModule('lib/client/main.js', 'client');
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'vulcan:events',
|
name: 'vulcan:events',
|
||||||
summary: 'Vulcan event tracking package',
|
summary: 'Vulcan event tracking package',
|
||||||
version: '1.12.17',
|
version: '1.13.0',
|
||||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||||
});
|
});
|
||||||
|
|
||||||
Package.onUse(function(api) {
|
Package.onUse(function(api) {
|
||||||
api.versionsFrom('1.6.1');
|
api.versionsFrom('1.6.1');
|
||||||
|
|
||||||
api.use(['vulcan:core@1.12.17']);
|
api.use(['vulcan:core@1.13.0']);
|
||||||
|
|
||||||
api.mainModule('lib/server/main.js', 'server');
|
api.mainModule('lib/server/main.js', 'server');
|
||||||
api.mainModule('lib/client/main.js', 'client');
|
api.mainModule('lib/client/main.js', 'client');
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'vulcan:forms-tags',
|
name: 'vulcan:forms-tags',
|
||||||
summary: 'Vulcan tag input package',
|
summary: 'Vulcan tag input package',
|
||||||
version: '1.12.17',
|
version: '1.13.0',
|
||||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||||
});
|
});
|
||||||
|
|
||||||
Package.onUse(function(api) {
|
Package.onUse(function(api) {
|
||||||
api.versionsFrom('1.6.1');
|
api.versionsFrom('1.6.1');
|
||||||
|
|
||||||
api.use(['vulcan:core@1.12.17', 'vulcan:forms@1.12.17']);
|
api.use(['vulcan:core@1.13.0', 'vulcan:forms@1.13.0']);
|
||||||
|
|
||||||
api.mainModule('lib/export.js', ['client', 'server']);
|
api.mainModule('lib/export.js', ['client', 'server']);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'vulcan:forms-upload',
|
name: 'vulcan:forms-upload',
|
||||||
summary: 'Vulcan package extending vulcan:forms to upload images to Cloudinary from a drop zone.',
|
summary: 'Vulcan package extending vulcan:forms to upload images to Cloudinary from a drop zone.',
|
||||||
version: '1.12.17',
|
version: '1.13.0',
|
||||||
git: 'https://github.com/xavcz/nova-forms-upload.git',
|
git: 'https://github.com/xavcz/nova-forms-upload.git',
|
||||||
});
|
});
|
||||||
|
|
||||||
Package.onUse(function(api) {
|
Package.onUse(function(api) {
|
||||||
api.versionsFrom('1.6.1');
|
api.versionsFrom('1.6.1');
|
||||||
|
|
||||||
api.use(['vulcan:core@1.12.17', 'vulcan:forms@1.12.17', 'fourseven:scss@4.10.0']);
|
api.use(['vulcan:core@1.13.0', 'vulcan:forms@1.13.0', 'fourseven:scss@4.10.0']);
|
||||||
|
|
||||||
api.addFiles(['lib/Upload.scss'], 'client');
|
api.addFiles(['lib/Upload.scss'], 'client');
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,7 @@ import { callbackProps } from './propTypes';
|
||||||
|
|
||||||
// props that should trigger a form reset
|
// props that should trigger a form reset
|
||||||
const RESET_PROPS = [
|
const RESET_PROPS = [
|
||||||
'collection', 'collectionName', 'typeName', 'document', 'schema', 'currentUser',
|
'collection', 'collectionName', 'typeName', 'document', 'schema', 'currentUser',
|
||||||
'fields', 'removeFields',
|
'fields', 'removeFields',
|
||||||
'prefilledProps' // TODO: prefilledProps should be merged instead?
|
'prefilledProps' // TODO: prefilledProps should be merged instead?
|
||||||
];
|
];
|
||||||
|
@ -104,17 +104,17 @@ const getInitialStateFromProps = nextProps => {
|
||||||
nextProps.prefilledProps,
|
nextProps.prefilledProps,
|
||||||
nextProps.document
|
nextProps.document
|
||||||
);
|
);
|
||||||
|
|
||||||
//if minCount is specified, go ahead and create empty nested documents
|
//if minCount is specified, go ahead and create empty nested documents
|
||||||
Object.keys(convertedSchema).forEach(key => {
|
Object.keys(convertedSchema).forEach(key => {
|
||||||
let minCount = convertedSchema[key].minCount;
|
let minCount = convertedSchema[key].minCount;
|
||||||
if(minCount) {
|
if (minCount) {
|
||||||
initialDocument[key] = initialDocument[key] || [];
|
initialDocument[key] = initialDocument[key] || [];
|
||||||
while(initialDocument[key].length < minCount)
|
while (initialDocument[key].length < minCount)
|
||||||
initialDocument[key].push({});
|
initialDocument[key].push({});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// remove all instances of the `__typename` property from document
|
// remove all instances of the `__typename` property from document
|
||||||
Utils.removeProperty(initialDocument, '__typename');
|
Utils.removeProperty(initialDocument, '__typename');
|
||||||
|
|
||||||
|
@ -154,7 +154,7 @@ class SmartForm extends Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultValues = {};
|
defaultValues = {};
|
||||||
|
|
||||||
submitFormCallbacks = [];
|
submitFormCallbacks = [];
|
||||||
successFormCallbacks = [];
|
successFormCallbacks = [];
|
||||||
|
@ -266,7 +266,7 @@ class SmartForm extends Component {
|
||||||
});
|
});
|
||||||
|
|
||||||
// run data object through submitForm callbacks
|
// run data object through submitForm callbacks
|
||||||
data = runCallbacks({ callbacks: this.submitFormCallbacks, iterator: data, properties: { form: this }});
|
data = runCallbacks({ callbacks: this.submitFormCallbacks, iterator: data, properties: { form: this } });
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
@ -737,8 +737,72 @@ class SmartForm extends Component {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
Warn the user if there are unsaved changes
|
Install a route leave hook to warn the user if there are unsaved changes
|
||||||
|
|
||||||
|
*/
|
||||||
|
componentDidMount = () => {
|
||||||
|
this.checkRouteChange();
|
||||||
|
this.checkBrowserClosing();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Remove the closing browser check on component unmount
|
||||||
|
see https://gist.github.com/mknabe/bfcb6db12ef52323954a28655801792d
|
||||||
|
*/
|
||||||
|
componentWillUnmount = () => {
|
||||||
|
if (this.getWarnUnsavedChanges()) {
|
||||||
|
// unblock route change
|
||||||
|
if (this.unblock) {
|
||||||
|
this.unblock();
|
||||||
|
}
|
||||||
|
// unblock browser change
|
||||||
|
window.onbeforeunload = undefined; //undefined instead of null to support IE
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------- Check on form leaving ----- //
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we must warn user on unsaved change
|
||||||
|
*/
|
||||||
|
getWarnUnsavedChanges = () => {
|
||||||
|
let warnUnsavedChanges = getSetting('forms.warnUnsavedChanges');
|
||||||
|
if (typeof this.props.warnUnsavedChanges === 'boolean') {
|
||||||
|
warnUnsavedChanges = this.props.warnUnsavedChanges;
|
||||||
|
}
|
||||||
|
return warnUnsavedChanges;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for route change, prevent form content loss
|
||||||
|
checkRouteChange = () => {
|
||||||
|
// @see https://github.com/ReactTraining/react-router/issues/4635#issuecomment-297828995
|
||||||
|
// @see https://github.com/ReactTraining/history#blocking-transitions
|
||||||
|
if (this.getWarnUnsavedChanges()) {
|
||||||
|
this.unblock = this.props.history.block((location, action) => {
|
||||||
|
// return the message that will pop into a window.confirm alert
|
||||||
|
// if returns nothing, the message won't appear and the user won't be blocked
|
||||||
|
return this.handleRouteLeave();
|
||||||
|
|
||||||
|
/*
|
||||||
|
// React-router 3 implementtion
|
||||||
|
const routes = this.props.router.routes;
|
||||||
|
const currentRoute = routes[routes.length - 1];
|
||||||
|
this.props.router.setRouteLeaveHook(currentRoute, this.handleRouteLeave);
|
||||||
|
|
||||||
|
*/
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// check for browser closing
|
||||||
|
checkBrowserClosing = () => {
|
||||||
|
//check for closing the browser with unsaved changes too
|
||||||
|
window.onbeforeunload = this.handlePageLeave;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Check if the user has unsaved changes, returns a message if yes
|
||||||
|
and nothing if not
|
||||||
*/
|
*/
|
||||||
handleRouteLeave = () => {
|
handleRouteLeave = () => {
|
||||||
if (this.isChanged()) {
|
if (this.isChanged()) {
|
||||||
|
@ -750,9 +814,13 @@ class SmartForm extends Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
//see https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload
|
/**
|
||||||
//the message returned is actually ignored by most browsers and a default message 'Are you sure you want to leave this page? You might have unsaved changes' is displayed. See the Notes section on the mozilla docs above
|
* Same for browser closing
|
||||||
handlePageLeave = event => {
|
*
|
||||||
|
* see https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload
|
||||||
|
* the message returned is actually ignored by most browsers and a default message 'Are you sure you want to leave this page? You might have unsaved changes' is displayed. See the Notes section on the mozilla docs above
|
||||||
|
*/
|
||||||
|
handlePageLeave = (event) => {
|
||||||
if (this.isChanged()) {
|
if (this.isChanged()) {
|
||||||
const message = this.context.intl.formatMessage({
|
const message = this.context.intl.formatMessage({
|
||||||
id: 'forms.confirm_discard',
|
id: 'forms.confirm_discard',
|
||||||
|
@ -765,41 +833,6 @@ class SmartForm extends Component {
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Install a route leave hook to warn the user if there are unsaved changes
|
|
||||||
|
|
||||||
*/
|
|
||||||
componentDidMount = () => {
|
|
||||||
let warnUnsavedChanges = getSetting('forms.warnUnsavedChanges');
|
|
||||||
if (typeof this.props.warnUnsavedChanges === 'boolean') {
|
|
||||||
warnUnsavedChanges = this.props.warnUnsavedChanges;
|
|
||||||
}
|
|
||||||
if (warnUnsavedChanges) {
|
|
||||||
const routes = this.props.router.routes;
|
|
||||||
const currentRoute = routes[routes.length - 1];
|
|
||||||
this.props.router.setRouteLeaveHook(currentRoute, this.handleRouteLeave);
|
|
||||||
|
|
||||||
//check for closing the browser with unsaved changes
|
|
||||||
window.onbeforeunload = this.handlePageLeave;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
Remove the closing browser check on component unmount
|
|
||||||
see https://gist.github.com/mknabe/bfcb6db12ef52323954a28655801792d
|
|
||||||
*/
|
|
||||||
componentWillUnmount = () => {
|
|
||||||
let warnUnsavedChanges = getSetting('forms.warnUnsavedChanges');
|
|
||||||
if (typeof this.props.warnUnsavedChanges === 'boolean') {
|
|
||||||
warnUnsavedChanges = this.props.warnUnsavedChanges;
|
|
||||||
}
|
|
||||||
if (warnUnsavedChanges) {
|
|
||||||
window.onbeforeunload = undefined; //undefined instead of null to support IE
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
Returns true if there are any differences between the initial document and the current one
|
Returns true if there are any differences between the initial document and the current one
|
||||||
|
@ -899,7 +932,7 @@ class SmartForm extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// run document through mutation success callbacks
|
// run document through mutation success callbacks
|
||||||
document = runCallbacks({ callbacks: this.successFormCallbacks, iterator: document, properties: { form: this }});
|
document = runCallbacks({ callbacks: this.successFormCallbacks, iterator: document, properties: { form: this } });
|
||||||
|
|
||||||
// run success callback if it exists
|
// run success callback if it exists
|
||||||
if (this.props.successCallback) this.props.successCallback(document, { form: this });
|
if (this.props.successCallback) this.props.successCallback(document, { form: this });
|
||||||
|
@ -915,7 +948,7 @@ class SmartForm extends Component {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
|
||||||
// run mutation failure callbacks on error, we do not allow the callbacks to change the error
|
// run mutation failure callbacks on error, we do not allow the callbacks to change the error
|
||||||
runCallbacks({ callbacks: this.failureFormCallbacks, iterator: error, properties: { error, form: this }});
|
runCallbacks({ callbacks: this.failureFormCallbacks, iterator: error, properties: { error, form: this } });
|
||||||
|
|
||||||
if (!_.isEmpty(error)) {
|
if (!_.isEmpty(error)) {
|
||||||
// add error to state
|
// add error to state
|
||||||
|
@ -937,7 +970,7 @@ class SmartForm extends Component {
|
||||||
submitForm = event => {
|
submitForm = event => {
|
||||||
|
|
||||||
event && event.preventDefault();
|
event && event.preventDefault();
|
||||||
|
|
||||||
// if form is disabled (there is already a submit handler running) don't do anything
|
// if form is disabled (there is already a submit handler running) don't do anything
|
||||||
if (this.state.disabled) {
|
if (this.state.disabled) {
|
||||||
return;
|
return;
|
||||||
|
@ -1003,7 +1036,7 @@ class SmartForm extends Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// --------------------------------------------------------------------- //
|
// --------------------------------------------------------------------- //
|
||||||
// ------------------------- Props to Pass ----------------------------- //
|
// ------------------------- Props to Pass ----------------------------- //
|
||||||
// --------------------------------------------------------------------- //
|
// --------------------------------------------------------------------- //
|
||||||
|
@ -1045,15 +1078,15 @@ class SmartForm extends Component {
|
||||||
cancelCallback: this.props.cancelCallback,
|
cancelCallback: this.props.cancelCallback,
|
||||||
revertCallback: this.props.revertCallback,
|
revertCallback: this.props.revertCallback,
|
||||||
document: this.getDocument(),
|
document: this.getDocument(),
|
||||||
deleteDocument:
|
deleteDocument:
|
||||||
(this.getFormType() === 'edit' &&
|
(this.getFormType() === 'edit' &&
|
||||||
this.props.showRemove &&
|
this.props.showRemove &&
|
||||||
this.deleteDocument) ||
|
this.deleteDocument) ||
|
||||||
null,
|
null,
|
||||||
collectionName:this.props.collectionName,
|
collectionName: this.props.collectionName,
|
||||||
currentValues:this.state.currentValues,
|
currentValues: this.state.currentValues,
|
||||||
deletedValues:this.state.deletedValues,
|
deletedValues: this.state.deletedValues,
|
||||||
errors:this.state.errors,
|
errors: this.state.errors,
|
||||||
});
|
});
|
||||||
|
|
||||||
// --------------------------------------------------------------------- //
|
// --------------------------------------------------------------------- //
|
||||||
|
|
14
packages/vulcan-forms/lib/components/FormElement.jsx
Normal file
14
packages/vulcan-forms/lib/components/FormElement.jsx
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { registerComponent } from 'meteor/vulcan:core';
|
||||||
|
|
||||||
|
// this component receives a ref, so it must be a class component
|
||||||
|
class FormElement extends React.Component {
|
||||||
|
render(){
|
||||||
|
const { children, ...otherProps } = this.props;
|
||||||
|
return <form {...otherProps}>{children}</form>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registerComponent({
|
||||||
|
name:'FormElement',
|
||||||
|
component: FormElement
|
||||||
|
});
|
|
@ -1,4 +1,5 @@
|
||||||
import '../components/FieldErrors.jsx';
|
import '../components/FieldErrors.jsx';
|
||||||
|
import '../components/FormElement.jsx';
|
||||||
import '../components/FormErrors.jsx';
|
import '../components/FormErrors.jsx';
|
||||||
import '../components/FormError.jsx';
|
import '../components/FormError.jsx';
|
||||||
import '../components/FormComponent.jsx';
|
import '../components/FormComponent.jsx';
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'vulcan:forms',
|
name: 'vulcan:forms',
|
||||||
summary: 'Form containers for React',
|
summary: 'Form containers for React',
|
||||||
version: '1.12.17',
|
version: '1.13.0',
|
||||||
git: 'https://github.com/meteor-utilities/react-form-containers.git',
|
git: 'https://github.com/meteor-utilities/react-form-containers.git',
|
||||||
});
|
});
|
||||||
|
|
||||||
Package.onUse(function(api) {
|
Package.onUse(function(api) {
|
||||||
api.versionsFrom('1.6.1');
|
api.versionsFrom('1.6.1');
|
||||||
|
|
||||||
api.use(['vulcan:core@1.12.17']);
|
api.use(['vulcan:core@1.13.0']);
|
||||||
|
|
||||||
api.mainModule('lib/client/main.js', ['client']);
|
api.mainModule('lib/client/main.js', ['client']);
|
||||||
api.mainModule('lib/server/main.js', ['server']);
|
api.mainModule('lib/server/main.js', ['server']);
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'vulcan:i18n-en-us',
|
name: 'vulcan:i18n-en-us',
|
||||||
summary: 'Vulcan i18n package (en_US)',
|
summary: 'Vulcan i18n package (en_US)',
|
||||||
version: '1.12.17',
|
version: '1.13.0',
|
||||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||||
});
|
});
|
||||||
|
|
||||||
Package.onUse(function(api) {
|
Package.onUse(function(api) {
|
||||||
api.versionsFrom('1.6.1');
|
api.versionsFrom('1.6.1');
|
||||||
|
|
||||||
api.use(['vulcan:core@1.12.17']);
|
api.use(['vulcan:core@1.13.0']);
|
||||||
|
|
||||||
api.addFiles(['lib/en_US.js'], ['client', 'server']);
|
api.addFiles(['lib/en_US.js'], ['client', 'server']);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'vulcan:i18n-es-es',
|
name: 'vulcan:i18n-es-es',
|
||||||
summary: 'Vulcan i18n package (es_ES)',
|
summary: 'Vulcan i18n package (es_ES)',
|
||||||
version: '1.12.17',
|
version: '1.13.0',
|
||||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||||
});
|
});
|
||||||
|
|
||||||
Package.onUse(function(api) {
|
Package.onUse(function(api) {
|
||||||
api.versionsFrom('1.6.1');
|
api.versionsFrom('1.6.1');
|
||||||
|
|
||||||
api.use(['vulcan:core@1.12.17']);
|
api.use(['vulcan:core@1.13.0']);
|
||||||
|
|
||||||
api.addFiles(['lib/es_ES.js'], ['client', 'server']);
|
api.addFiles(['lib/es_ES.js'], ['client', 'server']);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'vulcan:i18n-fr-fr',
|
name: 'vulcan:i18n-fr-fr',
|
||||||
summary: 'Vulcan i18n package (fr_FR)',
|
summary: 'Vulcan i18n package (fr_FR)',
|
||||||
version: '1.12.17',
|
version: '1.13.0',
|
||||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||||
});
|
});
|
||||||
|
|
||||||
Package.onUse(function(api) {
|
Package.onUse(function(api) {
|
||||||
api.versionsFrom('1.6.1');
|
api.versionsFrom('1.6.1');
|
||||||
|
|
||||||
api.use(['vulcan:core@1.12.17']);
|
api.use(['vulcan:core@1.13.0']);
|
||||||
|
|
||||||
api.addFiles(['lib/fr_FR.js'], ['client', 'server']);
|
api.addFiles(['lib/fr_FR.js'], ['client', 'server']);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'vulcan:i18n',
|
name: 'vulcan:i18n',
|
||||||
summary: 'i18n client polyfill',
|
summary: 'i18n client polyfill',
|
||||||
version: '1.12.17',
|
version: '1.13.0',
|
||||||
git: 'https://github.com/VulcanJS/Vulcan',
|
git: 'https://github.com/VulcanJS/Vulcan',
|
||||||
});
|
});
|
||||||
|
|
||||||
Package.onUse(function(api) {
|
Package.onUse(function(api) {
|
||||||
api.versionsFrom('1.6.1');
|
api.versionsFrom('1.6.1');
|
||||||
|
|
||||||
api.use(['vulcan:lib@1.12.17']);
|
api.use(['vulcan:lib@1.13.0']);
|
||||||
|
|
||||||
api.mainModule('lib/server/main.js', 'server');
|
api.mainModule('lib/server/main.js', 'server');
|
||||||
api.mainModule('lib/client/main.js', 'client');
|
api.mainModule('lib/client/main.js', 'client');
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { onError } from 'apollo-link-error';
|
import { onError } from 'apollo-link-error';
|
||||||
|
|
||||||
|
const locationsToStr = (locations=[]) => locations.map(({column, line}) => `line ${line}, col ${column}`).join(';');
|
||||||
const errorLink = onError(({ graphQLErrors, networkError }) => {
|
const errorLink = onError(({ graphQLErrors, networkError }) => {
|
||||||
if (graphQLErrors)
|
if (graphQLErrors)
|
||||||
graphQLErrors.map(({ message, locations, path }) => {
|
graphQLErrors.map(({ message, locations, path }) => {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`);
|
console.log(`[GraphQL error]: Message: ${message}, Location: ${locationsToStr(locations)}, Path: ${path}`);
|
||||||
});
|
});
|
||||||
if (networkError) {
|
if (networkError) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
|
|
@ -9,7 +9,7 @@ import SimpleSchema from 'simpl-schema';
|
||||||
Vulcan = {};
|
Vulcan = {};
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
Vulcan.VERSION = '1.12.17';
|
Vulcan.VERSION = '1.13.0';
|
||||||
|
|
||||||
// ------------------------------------- Schemas -------------------------------- //
|
// ------------------------------------- Schemas -------------------------------- //
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,8 @@ const getFirstWord = input => {
|
||||||
|
|
||||||
Parse a GraphQL error message
|
Parse a GraphQL error message
|
||||||
|
|
||||||
|
TODO: check if still useful?
|
||||||
|
|
||||||
Sample message:
|
Sample message:
|
||||||
|
|
||||||
"GraphQL error: Variable "$data" got invalid value {"meetingDate":"2018-08-07T06:05:51.704Z"}.
|
"GraphQL error: Variable "$data" got invalid value {"meetingDate":"2018-08-07T06:05:51.704Z"}.
|
||||||
|
@ -28,6 +30,11 @@ In field "addresses": Expected "[JSON]!", found null."
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const parseErrorMessage = message => {
|
const parseErrorMessage = message => {
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// note: optionally add .slice(1) at the end to get rid of the first error, which is not that helpful
|
// note: optionally add .slice(1) at the end to get rid of the first error, which is not that helpful
|
||||||
let fieldErrors = message.split('\n');
|
let fieldErrors = message.split('\n');
|
||||||
|
|
||||||
|
@ -52,6 +59,7 @@ const parseErrorMessage = message => {
|
||||||
});
|
});
|
||||||
return fieldErrors;
|
return fieldErrors;
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
Errors can have the following properties stored on their `data` property:
|
Errors can have the following properties stored on their `data` property:
|
||||||
|
@ -60,37 +68,17 @@ Errors can have the following properties stored on their `data` property:
|
||||||
- properties: additional data. Will be passed to vulcan-i18n as values
|
- properties: additional data. Will be passed to vulcan-i18n as values
|
||||||
- message: if id cannot be used as i81n key, message will be used
|
- message: if id cannot be used as i81n key, message will be used
|
||||||
|
|
||||||
Scenario 1: normal error thrown with new Error(), put it in array and return it
|
|
||||||
|
|
||||||
Scenario 2: multiple GraphQL errors stored on data.errors
|
|
||||||
|
|
||||||
Scenario 3: single GraphQL error with data property
|
|
||||||
|
|
||||||
Scenario 4: single GraphQL error with no data property
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
export const getErrors = error => {
|
export const getErrors = error => {
|
||||||
|
|
||||||
// 1. by default, return raw error wrapped in array
|
const graphQLErrors = error.graphQLErrors;
|
||||||
let errors = [error];
|
|
||||||
|
// error thrown using new ApolloError
|
||||||
|
const apolloErrors = get(graphQLErrors, '0.extensions.exception.data.errors');
|
||||||
|
|
||||||
// if this is one or more GraphQL errors, extract and convert them
|
// regular server error (with schema stitching)
|
||||||
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
const regularErrors = get(graphQLErrors, '0.extensions.exception.errors');
|
||||||
// get first graphQL error (see https://github.com/thebigredgeek/apollo-errors/issues/12)
|
|
||||||
const graphQLError = error.graphQLErrors[0];
|
return apolloErrors || regularErrors || graphQLErrors;
|
||||||
const data = get(graphQLError, 'extensions.exception.data')
|
|
||||||
if (data && !isEmpty(data)) {
|
|
||||||
if (data.errors) {
|
|
||||||
// 2. there are multiple errors on the data.errors object
|
|
||||||
errors = data.errors;
|
|
||||||
} else {
|
|
||||||
// 3. there is only one error
|
|
||||||
errors = [data];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 4. there is no data object, try to parse raw error message
|
|
||||||
errors = parseErrorMessage(graphQLError.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return errors;
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export * from './apollo_server2';
|
export * from './apollo_server';
|
||||||
export * from './settings';
|
export * from './settings';
|
||||||
|
|
||||||
export { default as initGraphQL } from './initGraphQL';
|
export { default as initGraphQL } from './initGraphQL';
|
||||||
|
|
|
@ -75,7 +75,8 @@ const initGraphQL = () => {
|
||||||
resolvers: GraphQLSchema.resolvers,
|
resolvers: GraphQLSchema.resolvers,
|
||||||
schemaDirectives: GraphQLSchema.directives,
|
schemaDirectives: GraphQLSchema.directives,
|
||||||
});
|
});
|
||||||
const mergedSchema = mergeSchemas({ schemas: [executableSchema, ...GraphQLSchema.stitchedSchemas] });
|
// only call mergeSchemas if we actually have stitchedSchemas
|
||||||
|
const mergedSchema = GraphQLSchema.stitchedSchemas.length > 0 ? mergeSchemas({ schemas: [executableSchema, ...GraphQLSchema.stitchedSchemas] }) : executableSchema;
|
||||||
|
|
||||||
GraphQLSchema.finalSchema = typeDefs;
|
GraphQLSchema.finalSchema = typeDefs;
|
||||||
GraphQLSchema.executableSchema = mergedSchema;
|
GraphQLSchema.executableSchema = mergedSchema;
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
const { onStart } = require('./apollo_server2');
|
const { onStart } = require('./apollo_server');
|
||||||
// createApolloServer when server startup
|
// createApolloServer when server startup
|
||||||
Meteor.startup(onStart);
|
Meteor.startup(onStart);
|
||||||
|
|
|
@ -1,280 +0,0 @@
|
||||||
/* import { graphqlExpress, graphiqlExpress } from 'apollo-server-express';
|
|
||||||
import bodyParser from 'body-parser';
|
|
||||||
import express from 'express';
|
|
||||||
import { makeExecutableSchema } from 'graphql-tools';
|
|
||||||
import deepmerge from 'deepmerge';
|
|
||||||
import DataLoader from 'dataloader';
|
|
||||||
import { formatError } from 'apollo-errors';
|
|
||||||
import compression from 'compression';
|
|
||||||
import { Meteor } from 'meteor/meteor';
|
|
||||||
import { check } from 'meteor/check';
|
|
||||||
// import { Accounts } from 'meteor/accounts-base';
|
|
||||||
import { Engine } from 'apollo-engine';
|
|
||||||
|
|
||||||
import { GraphQLSchema } from '../modules/graphql.js';
|
|
||||||
import { Utils } from '../modules/utils.js';
|
|
||||||
import { webAppConnectHandlersUse } from './meteor_patch.js';
|
|
||||||
|
|
||||||
import { getSetting, registerSetting } from '../modules/settings.js';
|
|
||||||
import { Collections } from '../modules/collections.js';
|
|
||||||
import findByIds from '../modules/findbyids.js';
|
|
||||||
import { runCallbacks } from '../modules/callbacks.js';
|
|
||||||
import cookiesMiddleware from 'universal-cookie-express';
|
|
||||||
// import Cookies from 'universal-cookie';
|
|
||||||
import { _hashLoginToken, _tokenExpiration } from './accounts_helpers';
|
|
||||||
import { getHeaderLocale } from './intl.js';
|
|
||||||
|
|
||||||
export let executableSchema;
|
|
||||||
|
|
||||||
registerSetting('apolloEngine.logLevel', 'INFO', 'Log level (one of INFO, DEBUG, WARN, ERROR');
|
|
||||||
registerSetting('apolloServer.tracing', Meteor.isDevelopment, 'Tracing by Apollo. Default is true on development and false on prod', true);
|
|
||||||
|
|
||||||
// see https://github.com/apollographql/apollo-cache-control
|
|
||||||
const engineApiKey = getSetting('apolloEngine.apiKey');
|
|
||||||
const engineLogLevel = getSetting('apolloEngine.logLevel', 'INFO');
|
|
||||||
const engineConfig = {
|
|
||||||
apiKey: engineApiKey,
|
|
||||||
// "origins": [
|
|
||||||
// {
|
|
||||||
// "http": {
|
|
||||||
// "url": "http://localhost:3000/graphql"
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// ],
|
|
||||||
'stores': [
|
|
||||||
{
|
|
||||||
'name': 'vulcanCache',
|
|
||||||
'inMemory': {
|
|
||||||
'cacheSize': 20000000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
// "sessionAuth": {
|
|
||||||
// "store": "embeddedCache",
|
|
||||||
// "header": "Authorization"
|
|
||||||
// },
|
|
||||||
// "frontends": [
|
|
||||||
// {
|
|
||||||
// "host": "127.0.0.1",
|
|
||||||
// "port": 3000,
|
|
||||||
// "endpoint": "/graphql",
|
|
||||||
// "extensions": {
|
|
||||||
// "strip": []
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// ],
|
|
||||||
'queryCache': {
|
|
||||||
'publicFullQueryStore': 'vulcanCache',
|
|
||||||
'privateFullQueryStore': 'vulcanCache'
|
|
||||||
},
|
|
||||||
// "reporting": {
|
|
||||||
// "endpointUrl": "https://engine-report.apollographql.com",
|
|
||||||
// "debugReports": true
|
|
||||||
// },
|
|
||||||
'logging': {
|
|
||||||
'level': engineLogLevel
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let engine;
|
|
||||||
if (engineApiKey) {
|
|
||||||
engine = new Engine({ engineConfig });
|
|
||||||
engine.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
// defaults
|
|
||||||
const defaultConfig = {
|
|
||||||
path: '/graphql',
|
|
||||||
maxAccountsCacheSizeInMB: 1,
|
|
||||||
graphiql: Meteor.isDevelopment,
|
|
||||||
graphiqlPath: '/graphiql',
|
|
||||||
graphiqlOptions: {
|
|
||||||
passHeader: "'Authorization': localStorage['Meteor.loginToken']", // eslint-disable-line quotes
|
|
||||||
},
|
|
||||||
configServer: (graphQLServer) => { },
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultOptions = {
|
|
||||||
formatError: e => ({
|
|
||||||
message: e.message,
|
|
||||||
locations: e.locations,
|
|
||||||
path: e.path,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (Meteor.isDevelopment) {
|
|
||||||
defaultOptions.debug = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// createApolloServer
|
|
||||||
const createApolloServer = (givenOptions = {}, givenConfig = {}) => {
|
|
||||||
const graphiqlOptions = { ...defaultConfig.graphiqlOptions, ...givenConfig.graphiqlOptions };
|
|
||||||
const config = { ...defaultConfig, ...givenConfig };
|
|
||||||
config.graphiqlOptions = graphiqlOptions;
|
|
||||||
|
|
||||||
const graphQLServer = express();
|
|
||||||
|
|
||||||
config.configServer(graphQLServer);
|
|
||||||
|
|
||||||
// Use Engine middleware
|
|
||||||
if (engineApiKey) {
|
|
||||||
graphQLServer.use(engine.expressMiddleware());
|
|
||||||
}
|
|
||||||
|
|
||||||
// cookies
|
|
||||||
graphQLServer.use(cookiesMiddleware());
|
|
||||||
|
|
||||||
// compression
|
|
||||||
graphQLServer.use(compression());
|
|
||||||
|
|
||||||
// GraphQL endpoint
|
|
||||||
graphQLServer.use(config.path, bodyParser.json({ limit: getSetting('apolloServer.jsonParserOptions.limit') }), graphqlExpress(async (req) => {
|
|
||||||
let options;
|
|
||||||
let user = null;
|
|
||||||
|
|
||||||
if (typeof givenOptions === 'function') {
|
|
||||||
options = givenOptions(req);
|
|
||||||
} else {
|
|
||||||
options = givenOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge in the defaults
|
|
||||||
options = { ...defaultOptions, ...options };
|
|
||||||
if (options.context) {
|
|
||||||
// don't mutate the context provided in options
|
|
||||||
options.context = { ...options.context };
|
|
||||||
} else {
|
|
||||||
options.context = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// enable tracing and caching
|
|
||||||
options.tracing = getSetting('apolloServer.tracing', Meteor.isDevelopment);
|
|
||||||
options.cacheControl = true;
|
|
||||||
|
|
||||||
// note: custom default resolver doesn't currently work
|
|
||||||
// see https://github.com/apollographql/apollo-server/issues/716
|
|
||||||
// options.fieldResolver = (source, args, context, info) => {
|
|
||||||
// return source[info.fieldName];
|
|
||||||
// }
|
|
||||||
|
|
||||||
// console.log('// apollo_server.js req.renderContext');
|
|
||||||
// console.log(req.renderContext);
|
|
||||||
// console.log('\n\n');
|
|
||||||
|
|
||||||
// Get the token from the header
|
|
||||||
if (req.headers.authorization) {
|
|
||||||
const token = req.headers.authorization;
|
|
||||||
check(token, String);
|
|
||||||
const hashedToken = _hashLoginToken(token);
|
|
||||||
|
|
||||||
// Get the user from the database
|
|
||||||
user = await Meteor.users.findOne(
|
|
||||||
{ 'services.resume.loginTokens.hashedToken': hashedToken },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (user) {
|
|
||||||
|
|
||||||
// identify user to any server-side analytics providers
|
|
||||||
runCallbacks('events.identify', user);
|
|
||||||
|
|
||||||
const loginToken = Utils.findWhere(user.services.resume.loginTokens, { hashedToken });
|
|
||||||
const expiresAt = _tokenExpiration(loginToken.when);
|
|
||||||
const isExpired = expiresAt < new Date();
|
|
||||||
|
|
||||||
if (!isExpired) {
|
|
||||||
options.context.userId = user._id;
|
|
||||||
options.context.currentUser = user;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//add the headers to the context
|
|
||||||
options.context.headers = req.headers;
|
|
||||||
|
|
||||||
|
|
||||||
// merge with custom context
|
|
||||||
options.context = deepmerge(options.context, GraphQLSchema.context);
|
|
||||||
|
|
||||||
// go over context and add Dataloader to each collection
|
|
||||||
Collections.forEach(collection => {
|
|
||||||
options.context[collection.options.collectionName].loader = new DataLoader(ids => findByIds(collection, ids, options.context), { cache: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
// look for headers either in renderContext (SSR) or req (normal request to the endpoint)
|
|
||||||
const headers = req.renderContext.originalHeaders || req.headers;
|
|
||||||
|
|
||||||
options.context.locale = getHeaderLocale(headers, user && user.locale);
|
|
||||||
|
|
||||||
if (headers.apikey && (headers.apikey === getSetting('vulcan.apiKey'))) {
|
|
||||||
options.context.currentUser = { isAdmin: true, isApiUser: true };
|
|
||||||
}
|
|
||||||
// console.log('// apollo_server.js isSSR?', !!req.renderContext.originalHeaders ? 'yes' : 'no');
|
|
||||||
// console.log('// apollo_server.js headers:');
|
|
||||||
// console.log(headers);
|
|
||||||
// console.log('// apollo_server.js final locale: ', options.context.locale);
|
|
||||||
// console.log('\n\n');
|
|
||||||
|
|
||||||
// add error formatting from apollo-errors
|
|
||||||
options.formatError = formatError;
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Start GraphiQL if enabled
|
|
||||||
if (config.graphiql) {
|
|
||||||
graphQLServer.use(config.graphiqlPath, graphiqlExpress({ ...config.graphiqlOptions, endpointURL: config.path }));
|
|
||||||
}
|
|
||||||
|
|
||||||
// This binds the specified paths to the Express server running Apollo + GraphiQL
|
|
||||||
webAppConnectHandlersUse(Meteor.bindEnvironment(graphQLServer), {
|
|
||||||
name: 'graphQLServerMiddleware_bindEnvironment',
|
|
||||||
order: 30,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// createApolloServer when server startup
|
|
||||||
Meteor.startup(() => {
|
|
||||||
|
|
||||||
runCallbacks('graphql.init.before');
|
|
||||||
|
|
||||||
// typeDefs
|
|
||||||
const generateTypeDefs = () => [`
|
|
||||||
scalar JSON
|
|
||||||
scalar Date
|
|
||||||
|
|
||||||
${GraphQLSchema.getAdditionalSchemas()}
|
|
||||||
|
|
||||||
${GraphQLSchema.getCollectionsSchemas()}
|
|
||||||
|
|
||||||
type Query {
|
|
||||||
|
|
||||||
${GraphQLSchema.queries.map(q => (
|
|
||||||
`${q.description ? ` # ${q.description}
|
|
||||||
` : ''} ${q.query}
|
|
||||||
`)).join('\n')}
|
|
||||||
}
|
|
||||||
|
|
||||||
${GraphQLSchema.mutations.length > 0 ? `type Mutation {
|
|
||||||
|
|
||||||
${GraphQLSchema.mutations.map(m => (
|
|
||||||
`${m.description ? ` # ${m.description}
|
|
||||||
` : ''} ${m.mutation}
|
|
||||||
`)).join('\n')}
|
|
||||||
}
|
|
||||||
` : ''}
|
|
||||||
`];
|
|
||||||
|
|
||||||
const typeDefs = generateTypeDefs();
|
|
||||||
|
|
||||||
GraphQLSchema.finalSchema = typeDefs;
|
|
||||||
|
|
||||||
executableSchema = makeExecutableSchema({
|
|
||||||
typeDefs,
|
|
||||||
resolvers: GraphQLSchema.resolvers,
|
|
||||||
schemaDirectives: GraphQLSchema.directives,
|
|
||||||
});
|
|
||||||
|
|
||||||
createApolloServer({
|
|
||||||
schema: executableSchema,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
*/
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ApolloError } from 'apollo-server';
|
import { UserInputError } from 'apollo-server';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
|
@ -11,5 +11,5 @@ An error should have:
|
||||||
*/
|
*/
|
||||||
export const throwError = error => {
|
export const throwError = error => {
|
||||||
const { id, } = error;
|
const { id, } = error;
|
||||||
throw new ApolloError(id, 'VALIDATION_ERROR', error);
|
throw new UserInputError(id, error);
|
||||||
};
|
};
|
||||||
|
|
|
@ -18,4 +18,6 @@ export * from './intl.js';
|
||||||
export * from './accounts_helpers.js';
|
export * from './accounts_helpers.js';
|
||||||
export * from './source_version.js';
|
export * from './source_version.js';
|
||||||
|
|
||||||
|
export * from './apollo-server/settings.js';
|
||||||
|
|
||||||
import './apollo-server/startup';
|
import './apollo-server/startup';
|
||||||
|
|
|
@ -80,7 +80,7 @@ export const createMutator = async ({
|
||||||
Note: keep newDocument for backwards compatibility
|
Note: keep newDocument for backwards compatibility
|
||||||
|
|
||||||
*/
|
*/
|
||||||
const properties = { data, currentUser, collection, context, document, newDocument: document };
|
const properties = { data, originalData: clone(data), currentUser, collection, context, document, newDocument: document, schema };
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
|
@ -273,7 +273,7 @@ export const updateMutator = async ({
|
||||||
Properties
|
Properties
|
||||||
|
|
||||||
*/
|
*/
|
||||||
const properties = { data, oldDocument, document, currentUser, collection, context };
|
const properties = { data, oldDocument, document, currentUser, collection, context, schema };
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
|
@ -473,7 +473,7 @@ export const deleteMutator = async ({
|
||||||
Properties
|
Properties
|
||||||
|
|
||||||
*/
|
*/
|
||||||
const properties = { document, currentUser, collection, context };
|
const properties = { document, currentUser, collection, context, schema };
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
|
@ -584,7 +584,7 @@ const startDebugMutator = (name, action, properties) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const endDebugMutator = (name, action, properties) => {
|
const endDebugMutator = (name, action, properties = {}) => {
|
||||||
Object.keys(properties).forEach(p => {
|
Object.keys(properties).forEach(p => {
|
||||||
debug(`// ${p}: `, properties[p]);
|
debug(`// ${p}: `, properties[p]);
|
||||||
});
|
});
|
||||||
|
|
|
@ -93,9 +93,10 @@ Meteor.startup(() => {
|
||||||
Collections.forEach(collection => {
|
Collections.forEach(collection => {
|
||||||
const typeName = collection.options.typeName;
|
const typeName = collection.options.typeName;
|
||||||
|
|
||||||
collection.queryOne = async (documentId, { fragmentName, fragmentText, context }) => {
|
collection.queryOne = async (documentIdOrSelector, { fragmentName, fragmentText, context }) => {
|
||||||
|
const selector = typeof documentIdOrSelector === 'string' ? { documentId: documentIdOrSelector } : documentIdOrSelector;
|
||||||
const query = buildQuery(collection, { fragmentName, fragmentText });
|
const query = buildQuery(collection, { fragmentName, fragmentText });
|
||||||
const result = await runQuery(query, { input: { selector: { documentId } } }, context);
|
const result = await runQuery(query, { input: { selector } }, context);
|
||||||
return result.data[Utils.camelCaseify(typeName)].result;
|
return result.data[Utils.camelCaseify(typeName)].result;
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'vulcan:lib',
|
name: 'vulcan:lib',
|
||||||
summary: 'Vulcan libraries.',
|
summary: 'Vulcan libraries.',
|
||||||
version: '1.12.17',
|
version: '1.13.0',
|
||||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { createApolloServer } from '../../lib/server/apollo-server/apollo_server2';
|
import { createApolloServer } from '../../lib/server/apollo-server/apollo_server';
|
||||||
//import initGraphQL from '../../lib/server/apollo-server/initGraphQL';
|
//import initGraphQL from '../../lib/server/apollo-server/initGraphQL';
|
||||||
//import { GraphQLSchema } from '../../lib/modules/graphql';
|
//import { GraphQLSchema } from '../../lib/modules/graphql';
|
||||||
import expect from 'expect';
|
import expect from 'expect';
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'vulcan:newsletter',
|
name: 'vulcan:newsletter',
|
||||||
summary: 'Vulcan email newsletter package',
|
summary: 'Vulcan email newsletter package',
|
||||||
version: '1.12.17',
|
version: '1.13.0',
|
||||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||||
});
|
});
|
||||||
|
|
||||||
Package.onUse(function(api) {
|
Package.onUse(function(api) {
|
||||||
api.versionsFrom('1.6.1');
|
api.versionsFrom('1.6.1');
|
||||||
|
|
||||||
api.use(['vulcan:core@1.12.17', 'vulcan:email@1.12.17']);
|
api.use(['vulcan:core@1.13.0', 'vulcan:email@1.13.0']);
|
||||||
|
|
||||||
api.mainModule('lib/server/main.js', 'server');
|
api.mainModule('lib/server/main.js', 'server');
|
||||||
api.mainModule('lib/client/main.js', 'client');
|
api.mainModule('lib/client/main.js', 'client');
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'vulcan:payments',
|
name: 'vulcan:payments',
|
||||||
summary: 'Vulcan payments package',
|
summary: 'Vulcan payments package',
|
||||||
version: '1.12.17',
|
version: '1.13.0',
|
||||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||||
});
|
});
|
||||||
|
|
||||||
Package.onUse(function(api) {
|
Package.onUse(function(api) {
|
||||||
api.versionsFrom('1.6.1');
|
api.versionsFrom('1.6.1');
|
||||||
|
|
||||||
api.use(['promise', 'vulcan:core@1.12.17', 'fourseven:scss@4.5.4']);
|
api.use(['promise', 'vulcan:core@1.13.0', 'fourseven:scss@4.5.4']);
|
||||||
|
|
||||||
api.mainModule('lib/server/main.js', 'server');
|
api.mainModule('lib/server/main.js', 'server');
|
||||||
api.mainModule('lib/client/main.js', 'client');
|
api.mainModule('lib/client/main.js', 'client');
|
||||||
|
|
1
packages/vulcan-redux/README.md
Normal file
1
packages/vulcan-redux/README.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Redux package.
|
|
@ -1,14 +1,14 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'vulcan:routing',
|
name: 'vulcan:routing',
|
||||||
summary: 'Vulcan router package',
|
summary: 'Vulcan router package',
|
||||||
version: '1.12.17',
|
version: '1.13.0',
|
||||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||||
});
|
});
|
||||||
|
|
||||||
Package.onUse(function(api) {
|
Package.onUse(function(api) {
|
||||||
api.versionsFrom('1.6.1');
|
api.versionsFrom('1.6.1');
|
||||||
|
|
||||||
api.use(['vulcan:lib@1.12.17']);
|
api.use(['vulcan:lib@1.13.0']);
|
||||||
|
|
||||||
api.mainModule('lib/server/main.js', 'server');
|
api.mainModule('lib/server/main.js', 'server');
|
||||||
api.mainModule('lib/client/main.js', 'client');
|
api.mainModule('lib/client/main.js', 'client');
|
||||||
|
|
1
packages/vulcan-styled-components/README.md
Normal file
1
packages/vulcan-styled-components/README.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Styled components package.
|
|
@ -1,7 +1,7 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'vulcan:subscribe',
|
name: 'vulcan:subscribe',
|
||||||
summary: 'Subscribe to posts, users, etc. to be notified of new activity',
|
summary: 'Subscribe to posts, users, etc. to be notified of new activity',
|
||||||
version: '1.12.17',
|
version: '1.13.0',
|
||||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -9,11 +9,11 @@ Package.onUse(function(api) {
|
||||||
api.versionsFrom('1.6.1');
|
api.versionsFrom('1.6.1');
|
||||||
|
|
||||||
api.use([
|
api.use([
|
||||||
'vulcan:core@1.12.17',
|
'vulcan:core@1.13.0',
|
||||||
// dependencies on posts, categories are done with nested imports to reduce explicit dependencies
|
// dependencies on posts, categories are done with nested imports to reduce explicit dependencies
|
||||||
]);
|
]);
|
||||||
|
|
||||||
api.use(['vulcan:posts@1.12.17', 'vulcan:comments@1.12.17', 'vulcan:categories@1.12.17'], {
|
api.use(['vulcan:posts@1.13.0', 'vulcan:comments@1.13.0', 'vulcan:categories@1.13.0'], {
|
||||||
weak: true,
|
weak: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
1
packages/vulcan-test/README.md
Normal file
1
packages/vulcan-test/README.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Test package.
|
|
@ -1,14 +1,14 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'vulcan:ui-bootstrap',
|
name: 'vulcan:ui-bootstrap',
|
||||||
summary: 'Vulcan Bootstrap UI components.',
|
summary: 'Vulcan Bootstrap UI components.',
|
||||||
version: '1.12.17',
|
version: '1.13.0',
|
||||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||||
});
|
});
|
||||||
|
|
||||||
Package.onUse(function(api) {
|
Package.onUse(function(api) {
|
||||||
api.versionsFrom('1.6.1');
|
api.versionsFrom('1.6.1');
|
||||||
|
|
||||||
api.use(['vulcan:lib@1.12.17', 'fourseven:scss@4.10.0']);
|
api.use(['vulcan:lib@1.13.0', 'fourseven:scss@4.10.0']);
|
||||||
|
|
||||||
api.addFiles(['lib/stylesheets/style.scss', 'lib/stylesheets/datetime.scss'], 'client');
|
api.addFiles(['lib/stylesheets/style.scss', 'lib/stylesheets/datetime.scss'], 'client');
|
||||||
|
|
||||||
|
|
6
packages/vulcan-ui-material/.gitignore
vendored
Normal file
6
packages/vulcan-ui-material/.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
npm-debug.log
|
||||||
|
node_modules
|
||||||
|
.idea/workspace.xml
|
||||||
|
|
||||||
|
### eslint-config
|
||||||
|
.eslintrc
|
89
packages/vulcan-ui-material/.versions
Normal file
89
packages/vulcan-ui-material/.versions
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
accounts-base@1.4.3
|
||||||
|
allow-deny@1.1.0
|
||||||
|
autoupdate@1.5.0
|
||||||
|
babel-compiler@7.2.4
|
||||||
|
babel-runtime@1.3.0
|
||||||
|
base64@1.0.11
|
||||||
|
binary-heap@1.0.11
|
||||||
|
blaze-tools@1.0.10
|
||||||
|
boilerplate-generator@1.6.0
|
||||||
|
buffer@0.0.0
|
||||||
|
caching-compiler@1.2.1
|
||||||
|
caching-html-compiler@1.1.3
|
||||||
|
callback-hook@1.1.0
|
||||||
|
check@1.3.1
|
||||||
|
ddp@1.4.0
|
||||||
|
ddp-client@2.3.3
|
||||||
|
ddp-common@1.4.0
|
||||||
|
ddp-rate-limiter@1.0.7
|
||||||
|
ddp-server@2.2.0
|
||||||
|
deps@1.0.12
|
||||||
|
diff-sequence@1.1.1
|
||||||
|
dynamic-import@0.5.0
|
||||||
|
ecmascript@0.12.4
|
||||||
|
ecmascript-runtime@0.7.0
|
||||||
|
ecmascript-runtime-client@0.8.0
|
||||||
|
ecmascript-runtime-server@0.7.1
|
||||||
|
ejson@1.1.0
|
||||||
|
email@1.2.3
|
||||||
|
erikdakoda:vulcan-material-ui@1.12.8_17
|
||||||
|
es5-shim@4.8.0
|
||||||
|
fetch@0.1.0
|
||||||
|
fourseven:scss@4.10.0
|
||||||
|
geojson-utils@1.0.10
|
||||||
|
hot-code-push@1.0.4
|
||||||
|
html-tools@1.0.11
|
||||||
|
htmljs@1.0.11
|
||||||
|
http@1.4.1
|
||||||
|
id-map@1.1.0
|
||||||
|
inter-process-messaging@0.1.0
|
||||||
|
localstorage@1.2.0
|
||||||
|
logging@1.1.20
|
||||||
|
meteor@1.9.2
|
||||||
|
meteorhacks:inject-initial@1.0.4
|
||||||
|
meteorhacks:picker@1.0.3
|
||||||
|
minifier-css@1.4.1
|
||||||
|
minifier-js@2.4.0
|
||||||
|
minimongo@1.4.5
|
||||||
|
modern-browsers@0.1.3
|
||||||
|
modules@0.13.0
|
||||||
|
modules-runtime@0.10.3
|
||||||
|
mongo@1.6.0
|
||||||
|
mongo-decimal@0.1.0
|
||||||
|
mongo-dev-server@1.1.0
|
||||||
|
mongo-id@1.0.7
|
||||||
|
npm-mongo@3.1.1
|
||||||
|
ordered-dict@1.1.0
|
||||||
|
percolatestudio:synced-cron@1.1.0
|
||||||
|
promise@0.11.2
|
||||||
|
random@1.1.0
|
||||||
|
rate-limit@1.0.9
|
||||||
|
reactive-dict@1.2.1
|
||||||
|
reactive-var@1.0.11
|
||||||
|
reload@1.2.0
|
||||||
|
retry@1.1.0
|
||||||
|
routepolicy@1.1.0
|
||||||
|
server-render@0.3.1
|
||||||
|
service-configuration@1.0.11
|
||||||
|
session@1.2.0
|
||||||
|
shell-server@0.4.0
|
||||||
|
socket-stream-client@0.2.2
|
||||||
|
spacebars-compiler@1.1.3
|
||||||
|
standard-minifier-css@1.5.2
|
||||||
|
standard-minifier-js@2.4.0
|
||||||
|
static-html@1.2.2
|
||||||
|
templating-tools@1.1.2
|
||||||
|
tracker@1.2.0
|
||||||
|
underscore@1.0.10
|
||||||
|
url@1.2.0
|
||||||
|
vulcan:accounts@1.12.8
|
||||||
|
vulcan:core@1.12.8
|
||||||
|
vulcan:debug@1.12.8
|
||||||
|
vulcan:email@1.12.8
|
||||||
|
vulcan:forms@1.12.8
|
||||||
|
vulcan:i18n@1.12.8
|
||||||
|
vulcan:lib@1.12.8
|
||||||
|
vulcan:routing@1.12.8
|
||||||
|
vulcan:users@1.12.8
|
||||||
|
webapp@1.7.1
|
||||||
|
webapp-hashing@1.0.9
|
18
packages/vulcan-ui-material/accounts.css
Normal file
18
packages/vulcan-ui-material/accounts.css
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
.accounts-ui .form-control {
|
||||||
|
color: rgba(0, 0, 0, 0.87);
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.87);
|
||||||
|
margin-bottom: 1px;
|
||||||
|
border-radius: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accounts-ui .form-control:focus {
|
||||||
|
border-bottom-width: 2px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
12
packages/vulcan-ui-material/en_US.js
Normal file
12
packages/vulcan-ui-material/en_US.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { addStrings } from 'meteor/vulcan:core';
|
||||||
|
|
||||||
|
|
||||||
|
addStrings('en', {
|
||||||
|
|
||||||
|
'search.search': 'Search',
|
||||||
|
'search.clear': 'Clear search',
|
||||||
|
'load_more.load_more': 'Load more',
|
||||||
|
'load_more.loaded_count': 'Loaded {count} of {totalCount}',
|
||||||
|
'load_more.loaded_all': '{totalCount, plural, =0 {No items} one {One item} other {# items}}',
|
||||||
|
|
||||||
|
});
|
16
packages/vulcan-ui-material/forms.css
Normal file
16
packages/vulcan-ui-material/forms.css
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
.form-nested-item {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-nested-item-inner {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-nested-item-remove {
|
||||||
|
padding-top: 8px;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-nested-item-remove > button {
|
||||||
|
margin-right: -4px;
|
||||||
|
}
|
7
packages/vulcan-ui-material/fr_FR.js
Normal file
7
packages/vulcan-ui-material/fr_FR.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { addStrings } from 'meteor/vulcan:core';
|
||||||
|
|
||||||
|
addStrings('fr', {
|
||||||
|
'search.search': 'Recherche',
|
||||||
|
'search.clear': 'Effacer la recherche',
|
||||||
|
'modal.close': 'Fermer',
|
||||||
|
});
|
109
packages/vulcan-ui-material/history.md
Normal file
109
packages/vulcan-ui-material/history.md
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
1.12.8_17 / 2019-02-02
|
||||||
|
======================
|
||||||
|
|
||||||
|
* TooltipIntl: Changed display from 'inline-block' to 'inherit' for more flexibility
|
||||||
|
* Countries: Added getRegionLabel function
|
||||||
|
|
||||||
|
1.12.8_16 / 2019-01-21
|
||||||
|
======================
|
||||||
|
|
||||||
|
* Countries: Fixed bug in validateRegion
|
||||||
|
|
||||||
|
1.12.8_15 / 2019-01-21
|
||||||
|
======================
|
||||||
|
|
||||||
|
* Countries: Fixed bug in validateRegion
|
||||||
|
|
||||||
|
1.12.8_14 / 2019-01-20
|
||||||
|
======================
|
||||||
|
|
||||||
|
* Countries: Added validateRegion function, which given a region value or label, will return the region value ('NY' or 'New York' => 'NY)
|
||||||
|
* The contents of countries is now exported - this may be refactored out of the core vulcan-material-ui as some point
|
||||||
|
|
||||||
|
1.12.8_13 / 2019-01-14
|
||||||
|
======================
|
||||||
|
|
||||||
|
* ModalTrigger: Added boolean dialogOverflow prop for use cases like popups that can go beyond the size of the dialog box
|
||||||
|
* MuiSuggest: Fixed bug - The disabled state was not displayed correctly
|
||||||
|
* MuiSuggest: Fixed bug - After selecting a suggestion, clicking on the control did not re-open the suggestions menu
|
||||||
|
|
||||||
|
1.12.8_12 / 2019-01-12
|
||||||
|
======================
|
||||||
|
|
||||||
|
* Upgraded to Meteor 1.8.0.2
|
||||||
|
|
||||||
|
1.12.8_11 / 2018-12-21
|
||||||
|
======================
|
||||||
|
|
||||||
|
* SearchInput: Added install autosize-input to readme
|
||||||
|
* Datatable: Fixed sorting delay
|
||||||
|
* Datatable: Added tableHeadCell class
|
||||||
|
* Datatable: Added cellClass column property, which can be a string or a function: column.cellClass({ column, document, currentUser })
|
||||||
|
|
||||||
|
1.12.8_10 / 2018-12-09
|
||||||
|
======================
|
||||||
|
|
||||||
|
* TooltipIntl: Added icon class
|
||||||
|
* FormGroupWithLine: Moved caret from the right side to next to the title
|
||||||
|
* Changed load_more.loaded_all string
|
||||||
|
|
||||||
|
1.12.8_9 / 2018-11-26
|
||||||
|
=====================
|
||||||
|
|
||||||
|
* Fixed bug that displayed invalid total count at the bottom of data tables
|
||||||
|
|
||||||
|
1.12.8_8 / 2018-11-23
|
||||||
|
=====================
|
||||||
|
|
||||||
|
* Improved the functionality of the LoadMore component
|
||||||
|
* The showNoMore property has been deprecated
|
||||||
|
* A showCount property has been added (true by default) that shows a count of loaded and total items
|
||||||
|
* The load more icon or button is displayed even when infiniteScroll is enabled
|
||||||
|
|
||||||
|
1.12.8_7 / 2018-11-10
|
||||||
|
=====================
|
||||||
|
|
||||||
|
* Fixed bug in Datatable.jsx
|
||||||
|
* Updated ReadMe
|
||||||
|
|
||||||
|
1.12.8_6 / 2018-11-06
|
||||||
|
=====================
|
||||||
|
|
||||||
|
* Fixed bug in Datatable.jsx
|
||||||
|
* Reduced spacing of form components
|
||||||
|
|
||||||
|
1.12.8_5 / 2018-10-31
|
||||||
|
=====================
|
||||||
|
|
||||||
|
* Fixed bugs in Datatable pagination
|
||||||
|
* Set Datatable paginate prop to false by default
|
||||||
|
|
||||||
|
1.12.8_4 / 2018-10-31
|
||||||
|
=====================
|
||||||
|
|
||||||
|
* Removed 'fr_FR.js' from package.js because any french strings loaded activates the french language
|
||||||
|
* Fixed delete button and its tooltips positioning in FormSubmit
|
||||||
|
* Added pagination to Datatable
|
||||||
|
|
||||||
|
1.12.8_2 / 2018-10-29
|
||||||
|
=====================
|
||||||
|
|
||||||
|
* Fixed localization in "clear search" tooltip
|
||||||
|
* Added name and aria-haspopup properties to the input component to improve compliance and facilitate UAT
|
||||||
|
* Replaced Date, Time and DateTime form controls with native controls as recommended by MUI.
|
||||||
|
The deprecated react-datetime version of the controls are still there as DateRdt, TimeRdt and DateTimeRdt, but they are not registered.
|
||||||
|
* Updated readme
|
||||||
|
|
||||||
|
1.12.8_1 / 2018-10-22
|
||||||
|
=====================
|
||||||
|
|
||||||
|
* Made form components compatible with new Form.formComponents property
|
||||||
|
|
||||||
|
1.12.8 / 2018-10-19
|
||||||
|
===================
|
||||||
|
|
||||||
|
* Made improvements to the search box, including keyboard shortcuts (s: focus search; c: clear search)
|
||||||
|
* Added support in TooltipIntl for tooltips in popovers
|
||||||
|
* Added action prop to ModalTrigger that enables a parent component to call openModal and closeModal
|
||||||
|
* Started using MUI tables in Card component
|
||||||
|
* Fixed bugs in MuiSuggest component
|
2
packages/vulcan-ui-material/lib/client/main.js
Normal file
2
packages/vulcan-ui-material/lib/client/main.js
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from '../modules/index';
|
||||||
|
import './wrapWithMuiTheme';
|
29
packages/vulcan-ui-material/lib/client/wrapWithMuiTheme.jsx
Normal file
29
packages/vulcan-ui-material/lib/client/wrapWithMuiTheme.jsx
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { addCallback, registerComponent, Components } from 'meteor/vulcan:core';
|
||||||
|
import MuiThemeProvider from '@material-ui/core/styles/MuiThemeProvider';
|
||||||
|
import { getCurrentTheme } from '../modules/themes';
|
||||||
|
import JssCleanup from '../components/theme/JssCleanup';
|
||||||
|
|
||||||
|
class ThemeProvider extends React.Component {
|
||||||
|
render() {
|
||||||
|
const theme = getCurrentTheme();
|
||||||
|
return (
|
||||||
|
<MuiThemeProvider theme={theme}>
|
||||||
|
<JssCleanup>{this.props.children}</JssCleanup>
|
||||||
|
</MuiThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerComponent('ThemeProvider', ThemeProvider);
|
||||||
|
|
||||||
|
function wrapWithMuiTheme(app) {
|
||||||
|
return (
|
||||||
|
<Components.ThemeProvider>
|
||||||
|
{app}
|
||||||
|
</Components.ThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
addCallback('router.client.wrapper', wrapWithMuiTheme);
|
46
packages/vulcan-ui-material/lib/components/accounts/AccountsButton.jsx
Executable file
46
packages/vulcan-ui-material/lib/components/accounts/AccountsButton.jsx
Executable file
|
@ -0,0 +1,46 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Button from '@material-ui/core/Button';
|
||||||
|
import { replaceComponent, Utils } from 'meteor/vulcan:core';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
|
||||||
|
export class AccountsButton extends Component {
|
||||||
|
render () {
|
||||||
|
|
||||||
|
const {
|
||||||
|
label,
|
||||||
|
type,
|
||||||
|
disabled = false,
|
||||||
|
className,
|
||||||
|
onClick
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant={type === 'link' ? 'text' : 'contained'}
|
||||||
|
size={type === 'link' ? 'small' : undefined}
|
||||||
|
color="primary"
|
||||||
|
className={classNames(`button-${Utils.slugify(label)}`, className)}
|
||||||
|
type={type}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={onClick}
|
||||||
|
disableRipple={true}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
AccountsButton.propTypes = {
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
type: PropTypes.oneOf(['link', 'submit', 'button']),
|
||||||
|
disabled: PropTypes.bool,
|
||||||
|
className: PropTypes.string,
|
||||||
|
onClick: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
replaceComponent('AccountsButton', AccountsButton);
|
48
packages/vulcan-ui-material/lib/components/accounts/AccountsButtons.jsx
Executable file
48
packages/vulcan-ui-material/lib/components/accounts/AccountsButtons.jsx
Executable file
|
@ -0,0 +1,48 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Components, replaceComponent } from 'meteor/vulcan:core';
|
||||||
|
import CardActions from '@material-ui/core/CardActions';
|
||||||
|
import withStyles from '@material-ui/core/styles/withStyles';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
|
||||||
|
const styles = theme => ({
|
||||||
|
root: {
|
||||||
|
flexDirection: 'row-reverse',
|
||||||
|
padding: theme.spacing.unit * 2,
|
||||||
|
height: 'auto',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export class AccountsButtons extends Component {
|
||||||
|
render () {
|
||||||
|
|
||||||
|
const {
|
||||||
|
classes,
|
||||||
|
buttons = {},
|
||||||
|
className = 'buttons',
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardActions className={classNames(classes.root, className)}>
|
||||||
|
{Object.keys(buttons).map((id, i) =>
|
||||||
|
<Components.AccountsButton {...buttons[id]} key={i}/>
|
||||||
|
)}
|
||||||
|
</CardActions>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
AccountsButtons.propTypes = {
|
||||||
|
classes: PropTypes.object.isRequired,
|
||||||
|
buttons: PropTypes.object,
|
||||||
|
className: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
AccountsButtons.displayName = 'AccountsButtons';
|
||||||
|
|
||||||
|
|
||||||
|
replaceComponent('AccountsButtons', AccountsButtons, [withStyles, styles]);
|
114
packages/vulcan-ui-material/lib/components/accounts/AccountsField.jsx
Executable file
114
packages/vulcan-ui-material/lib/components/accounts/AccountsField.jsx
Executable file
|
@ -0,0 +1,114 @@
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { replaceComponent } from 'meteor/vulcan:core';
|
||||||
|
import TextField from '@material-ui/core/TextField';
|
||||||
|
|
||||||
|
|
||||||
|
const autocompleteValues = {
|
||||||
|
'username': 'username',
|
||||||
|
'usernameOrEmail': 'email',
|
||||||
|
'email': 'email',
|
||||||
|
'password': 'current-password'
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export class AccountsField extends PureComponent {
|
||||||
|
|
||||||
|
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
mount: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
triggerUpdate () {
|
||||||
|
// Trigger an onChange on initial load, to support browser pre-filled values.
|
||||||
|
const { onChange } = this.props;
|
||||||
|
if (this.input && onChange) {
|
||||||
|
onChange({ target: { value: this.input.value } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
componentDidUpdate (prevProps) {
|
||||||
|
// Re-mount component so that we don't expose browser pre-filled passwords if the component was
|
||||||
|
// a password before and now something else.
|
||||||
|
if (prevProps.id !== this.props.id) {
|
||||||
|
this.setState({ mount: false });
|
||||||
|
} else if (!this.state.mount) {
|
||||||
|
this.setState({ mount: true });
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
hint,
|
||||||
|
label,
|
||||||
|
type = 'text',
|
||||||
|
onChange,
|
||||||
|
required = false,
|
||||||
|
className = 'field',
|
||||||
|
defaultValue = '',
|
||||||
|
autoFocus,
|
||||||
|
messages,
|
||||||
|
} = this.props;
|
||||||
|
let { message } = this.props;
|
||||||
|
const { mount = true } = this.state;
|
||||||
|
|
||||||
|
if (type === 'notice') {
|
||||||
|
return <div className={className}>{label}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const autoComplete = autocompleteValues[id];
|
||||||
|
|
||||||
|
if (messages && messages.find && typeof id === 'string') {
|
||||||
|
const foundMessage = messages.find(element => {
|
||||||
|
if (typeof element.field !== 'string') return false;
|
||||||
|
return id.toLowerCase().indexOf(element.field.toLowerCase()) > -1;
|
||||||
|
});
|
||||||
|
if (foundMessage) {
|
||||||
|
message = foundMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
mount &&
|
||||||
|
|
||||||
|
<div className={className} style={{ marginBottom: '10px' }}>
|
||||||
|
<TextField
|
||||||
|
id={id}
|
||||||
|
type={type}
|
||||||
|
inputRef={ref => { this.input = ref; }}
|
||||||
|
onChange={onChange}
|
||||||
|
placeholder={hint}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
autoComplete={autoComplete }
|
||||||
|
label={label}
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
required={required}
|
||||||
|
error={!!message}
|
||||||
|
helperText={message && message.message}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
AccountsField.propTypes = {
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
replaceComponent('AccountsField', AccountsField);
|
27
packages/vulcan-ui-material/lib/components/accounts/AccountsFields.jsx
Executable file
27
packages/vulcan-ui-material/lib/components/accounts/AccountsFields.jsx
Executable file
|
@ -0,0 +1,27 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { Components, replaceComponent } from 'meteor/vulcan:core';
|
||||||
|
import CardContent from '@material-ui/core/CardContent';
|
||||||
|
|
||||||
|
|
||||||
|
export class AccountsFields extends Component {
|
||||||
|
render () {
|
||||||
|
const {
|
||||||
|
fields = {},
|
||||||
|
className = 'fields',
|
||||||
|
messages,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardContent className={className}>
|
||||||
|
{
|
||||||
|
Object.keys(fields).map((id, i) =>
|
||||||
|
<Components.AccountsField {...fields[id]} messages={messages} autoFocus={i === 0} key={i}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</CardContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
replaceComponent('AccountsFields', AccountsFields);
|
68
packages/vulcan-ui-material/lib/components/accounts/AccountsForm.jsx
Executable file
68
packages/vulcan-ui-material/lib/components/accounts/AccountsForm.jsx
Executable file
|
@ -0,0 +1,68 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { Components, registerComponent } from 'meteor/vulcan:core';
|
||||||
|
import withStyles from '@material-ui/core/styles/withStyles';
|
||||||
|
|
||||||
|
|
||||||
|
const styles = theme => ({
|
||||||
|
messages: theme.utils.errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export class AccountsForm extends Component {
|
||||||
|
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
let form = this.form;
|
||||||
|
if (form) {
|
||||||
|
form.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const {
|
||||||
|
oauthServices,
|
||||||
|
fields,
|
||||||
|
buttons,
|
||||||
|
messages,
|
||||||
|
ready = true,
|
||||||
|
className,
|
||||||
|
classes,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form ref={(ref) => this.form = ref}
|
||||||
|
className={classNames(className, 'accounts-ui', { 'ready': ready, })}
|
||||||
|
noValidate
|
||||||
|
>
|
||||||
|
<Components.AccountsFields fields={fields} messages={messages}/>
|
||||||
|
<Components.AccountsButtons buttons={{...buttons}}/>
|
||||||
|
<Components.AccountsPasswordOrService oauthServices={oauthServices}/>
|
||||||
|
<Components.AccountsSocialButtons oauthServices={oauthServices}/>
|
||||||
|
<Components.AccountsFormMessages messages={messages} className={classes.messages}/>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
AccountsForm.propTypes = {
|
||||||
|
oauthServices: PropTypes.object,
|
||||||
|
fields: PropTypes.object.isRequired,
|
||||||
|
buttons: PropTypes.object.isRequired,
|
||||||
|
error: PropTypes.string,
|
||||||
|
ready: PropTypes.bool,
|
||||||
|
classes: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
AccountsForm.displayName = 'AccountsForm';
|
||||||
|
|
||||||
|
|
||||||
|
registerComponent('AccountsForm', AccountsForm, [withStyles, styles]);
|
|
@ -0,0 +1,57 @@
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { replaceComponent } from 'meteor/vulcan:core';
|
||||||
|
import { intlShape } from 'meteor/vulcan:i18n';
|
||||||
|
import Typography from '@material-ui/core/Typography';
|
||||||
|
import CardActions from '@material-ui/core/CardActions';
|
||||||
|
import withStyles from '@material-ui/core/styles/withStyles';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
const styles = theme => ({
|
||||||
|
root: {
|
||||||
|
flexDirection: 'row-reverse',
|
||||||
|
paddingRight: theme.spacing.unit * 2,
|
||||||
|
paddingLeft: theme.spacing.unit * 2,
|
||||||
|
height: 'auto',
|
||||||
|
},
|
||||||
|
typography: {
|
||||||
|
marginRight: theme.spacing.unit,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function hasPasswordService() {
|
||||||
|
// First look for OAuth services.
|
||||||
|
return !!Package['accounts-password'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AccountsPasswordOrService extends PureComponent {
|
||||||
|
render () {
|
||||||
|
let { className = 'password-or-service', style = {}, classes } = this.props;
|
||||||
|
const services = Object.keys(this.props.oauthServices).map(service => {
|
||||||
|
return this.props.oauthServices[service].label;
|
||||||
|
});
|
||||||
|
let labels = services;
|
||||||
|
if (services.length > 2) {
|
||||||
|
labels = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasPasswordService() && services.length > 0) {
|
||||||
|
return (<CardActions className={classNames(className, classes.root)}>
|
||||||
|
<Typography variant="caption" className={classes.typography} align="right">
|
||||||
|
{ `${this.context.intl.formatMessage({id: 'accounts.or_use'})} ${ labels.join(' / ') }` }
|
||||||
|
</Typography></CardActions>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AccountsPasswordOrService.propTypes = {
|
||||||
|
oauthServices: PropTypes.object
|
||||||
|
};
|
||||||
|
|
||||||
|
AccountsPasswordOrService.contextTypes = {
|
||||||
|
intl: intlShape
|
||||||
|
};
|
||||||
|
|
||||||
|
replaceComponent('AccountsPasswordOrService', AccountsPasswordOrService, [withStyles, styles]);
|
|
@ -0,0 +1,28 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Components, replaceComponent } from 'meteor/vulcan:core';
|
||||||
|
import CardActions from '@material-ui/core/CardActions';
|
||||||
|
import withStyles from '@material-ui/core/styles/withStyles';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
const styles = theme => ({
|
||||||
|
root: {
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
padding: theme.spacing.unit * 2,
|
||||||
|
height: 'auto',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export class AccountsSocialButtons extends React.Component {
|
||||||
|
render() {
|
||||||
|
let { oauthServices = {}, className = 'social-buttons', classes } = this.props;
|
||||||
|
return(
|
||||||
|
<CardActions className={classNames(classes.root, className)}>
|
||||||
|
{Object.keys(oauthServices).map((id, i) => {
|
||||||
|
return <Components.AccountsButton {...oauthServices[id]} key={i} />;
|
||||||
|
})}
|
||||||
|
</CardActions>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceComponent('AccountsSocialButtons', AccountsSocialButtons, [withStyles, styles]);
|
133
packages/vulcan-ui-material/lib/components/bonus/LoadMore.jsx
Normal file
133
packages/vulcan-ui-material/lib/components/bonus/LoadMore.jsx
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { FormattedMessage, intlShape } from 'react-intl';
|
||||||
|
import { Components, registerComponent } from 'meteor/vulcan:core';
|
||||||
|
import withStyles from '@material-ui/core/styles/withStyles';
|
||||||
|
import Typography from '@material-ui/core/Typography';
|
||||||
|
import Button from '@material-ui/core/Button';
|
||||||
|
import IconButton from '@material-ui/core/IconButton';
|
||||||
|
import ArrowDownIcon from 'mdi-material-ui/ArrowDown';
|
||||||
|
import ScrollTrigger from './ScrollTrigger';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
|
||||||
|
const styles = theme => ({
|
||||||
|
|
||||||
|
root: {
|
||||||
|
textAlign: 'center',
|
||||||
|
flexBasis: '100%',
|
||||||
|
},
|
||||||
|
|
||||||
|
textButton: {
|
||||||
|
marginTop: theme.spacing.unit * 2,
|
||||||
|
},
|
||||||
|
|
||||||
|
iconButton: {},
|
||||||
|
|
||||||
|
caption: {
|
||||||
|
marginTop: theme.spacing.unit * 3,
|
||||||
|
paddingTop: theme.spacing.unit,
|
||||||
|
paddingBottom: theme.spacing.unit,
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const LoadMore = ({
|
||||||
|
classes,
|
||||||
|
count,
|
||||||
|
totalCount,
|
||||||
|
loadMore,
|
||||||
|
networkStatus,
|
||||||
|
showCount,
|
||||||
|
useTextButton,
|
||||||
|
className,
|
||||||
|
infiniteScroll,
|
||||||
|
}, { intl }) => {
|
||||||
|
|
||||||
|
const isLoadingMore = networkStatus === 2;
|
||||||
|
const loadMoreText = intl.formatMessage({ id: 'load_more.load_more' });
|
||||||
|
const title = `${loadMoreText} (${count}/${totalCount})`;
|
||||||
|
const hasMore = totalCount > count;
|
||||||
|
const countValues = { count, totalCount };
|
||||||
|
|
||||||
|
const loadMoreButton = useTextButton
|
||||||
|
?
|
||||||
|
<Button className={classes.textButton} onClick={() => loadMore()}>
|
||||||
|
{title}
|
||||||
|
</Button>
|
||||||
|
:
|
||||||
|
<IconButton className={classes.iconButton} onClick={() => loadMore()}>
|
||||||
|
<ArrowDownIcon/>
|
||||||
|
</IconButton>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames('load-more', classes.root, className)}>
|
||||||
|
{
|
||||||
|
showCount &&
|
||||||
|
|
||||||
|
<Typography variant="caption" className={classes.caption}>
|
||||||
|
<FormattedMessage id={`load_more.${hasMore ? 'loaded_count' : 'loaded_all'}`} values={countValues}/>
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
isLoadingMore
|
||||||
|
|
||||||
|
?
|
||||||
|
|
||||||
|
<Components.Loading/>
|
||||||
|
|
||||||
|
:
|
||||||
|
|
||||||
|
hasMore
|
||||||
|
|
||||||
|
?
|
||||||
|
|
||||||
|
infiniteScroll
|
||||||
|
|
||||||
|
?
|
||||||
|
|
||||||
|
<ScrollTrigger onEnter={() => loadMore()}>
|
||||||
|
{loadMoreButton}
|
||||||
|
</ScrollTrigger>
|
||||||
|
|
||||||
|
:
|
||||||
|
|
||||||
|
loadMoreButton
|
||||||
|
:
|
||||||
|
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
LoadMore.propTypes = {
|
||||||
|
classes: PropTypes.object.isRequired,
|
||||||
|
count: PropTypes.number,
|
||||||
|
totalCount: PropTypes.number,
|
||||||
|
loadMore: PropTypes.func,
|
||||||
|
networkStatus: PropTypes.number,
|
||||||
|
showCount: PropTypes.bool,
|
||||||
|
useTextButton: PropTypes.bool,
|
||||||
|
className: PropTypes.string,
|
||||||
|
infiniteScroll: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
LoadMore.defaultProps = {
|
||||||
|
showCount: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
LoadMore.contextTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
LoadMore.displayName = 'LoadMore';
|
||||||
|
|
||||||
|
|
||||||
|
registerComponent('LoadMore', LoadMore, [withStyles, styles]);
|
||||||
|
|
|
@ -0,0 +1,125 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import _throttle from 'lodash/throttle';
|
||||||
|
|
||||||
|
|
||||||
|
class ScrollTrigger extends Component {
|
||||||
|
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.onScroll = _throttle(this.onScroll.bind(this), 100, {
|
||||||
|
leading: true,
|
||||||
|
trailing: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.onResize = _throttle(this.onResize.bind(this), 100, {
|
||||||
|
leading: true,
|
||||||
|
trailing: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.inViewport = false;
|
||||||
|
this.passive = this.supportsPassive ? { passive: true } : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
supportsPassive () {
|
||||||
|
let supportsPassive = false;
|
||||||
|
try {
|
||||||
|
const opts = Object.defineProperty({}, 'passive', {
|
||||||
|
get: function() {
|
||||||
|
supportsPassive = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.addEventListener('testPassive', null, opts);
|
||||||
|
window.removeEventListener('testPassive', null, opts);
|
||||||
|
//eslint-disable-next-line no-empty
|
||||||
|
} catch (e) {}
|
||||||
|
return supportsPassive;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
this.scroller = document.getElementById('main');
|
||||||
|
this.scroller.addEventListener('resize', this.onResize, this.passive);
|
||||||
|
this.scroller.addEventListener('scroll', this.onScroll, this.passive);
|
||||||
|
|
||||||
|
this.inViewport = false;
|
||||||
|
|
||||||
|
if (this.props.triggerOnLoad) {
|
||||||
|
this.checkStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
if (!this.scroller) return;
|
||||||
|
this.scroller.removeEventListener('resize', this.onResize);
|
||||||
|
this.scroller.removeEventListener('scroll', this.onScroll);
|
||||||
|
this.scroller = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
onResize () {
|
||||||
|
this.checkStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
onScroll () {
|
||||||
|
this.checkStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
checkStatus () {
|
||||||
|
if (!this.scroller) return;
|
||||||
|
|
||||||
|
const {
|
||||||
|
onEnter,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
//eslint-disable-next-line
|
||||||
|
const element = ReactDOM.findDOMNode(this.element);
|
||||||
|
const elementRect = element.getBoundingClientRect();
|
||||||
|
const viewportEnd = this.scroller.clientHeight + this.props.preload;
|
||||||
|
const inViewport = elementRect.top < viewportEnd;
|
||||||
|
|
||||||
|
if (inViewport) {
|
||||||
|
if (!this.inViewport) {
|
||||||
|
this.inViewport = true;
|
||||||
|
|
||||||
|
onEnter(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (this.inViewport) {
|
||||||
|
this.inViewport = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const {
|
||||||
|
children,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={(element) => {this.element = element;}}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ScrollTrigger.propTypes = {
|
||||||
|
scrollerId: PropTypes.string,
|
||||||
|
triggerOnLoad: PropTypes.bool,
|
||||||
|
preload: PropTypes.number,
|
||||||
|
onEnter: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
ScrollTrigger.defaultProps = {
|
||||||
|
scrollerId: 'main',
|
||||||
|
preload: 1000,
|
||||||
|
triggerOnLoad: true,
|
||||||
|
onEnter: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default ScrollTrigger;
|
228
packages/vulcan-ui-material/lib/components/bonus/SearchInput.jsx
Normal file
228
packages/vulcan-ui-material/lib/components/bonus/SearchInput.jsx
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Components, registerComponent } from 'meteor/vulcan:core';
|
||||||
|
import withStyles from '@material-ui/core/styles/withStyles';
|
||||||
|
import SearchIcon from 'mdi-material-ui/Magnify';
|
||||||
|
import ClearIcon from 'mdi-material-ui/CloseCircle';
|
||||||
|
import TextField from '@material-ui/core/TextField';
|
||||||
|
import NoSsr from '@material-ui/core/NoSsr';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import _debounce from 'lodash/debounce';
|
||||||
|
import KeyboardEventHandler from 'react-keyboard-event-handler';
|
||||||
|
|
||||||
|
const styles = theme => ({
|
||||||
|
|
||||||
|
'@global': {
|
||||||
|
'input[type=text]::-ms-clear, input[type=text]::-ms-reveal':
|
||||||
|
{
|
||||||
|
display: 'none',
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
},
|
||||||
|
'input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button':
|
||||||
|
{ display: 'none' },
|
||||||
|
'input[type="search"]::-webkit-search-results-button, input[type="search"]::-webkit-search-results-decoration':
|
||||||
|
{ display: 'none' },
|
||||||
|
},
|
||||||
|
|
||||||
|
root: {
|
||||||
|
marginTop: 0
|
||||||
|
},
|
||||||
|
|
||||||
|
clear: {
|
||||||
|
transition: theme.transitions.create('opacity,transform', {
|
||||||
|
duration: theme.transitions.duration.short,
|
||||||
|
}),
|
||||||
|
opacity: 0.65,
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
margin: -6,
|
||||||
|
marginLeft: 0,
|
||||||
|
'& svg': {
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
},
|
||||||
|
flexDirection: 'column',
|
||||||
|
},
|
||||||
|
|
||||||
|
clearDense: {
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
margin: -4,
|
||||||
|
marginLeft: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
clearDisabled: {
|
||||||
|
opacity: 0,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
},
|
||||||
|
|
||||||
|
icon: {
|
||||||
|
color: theme.palette.common.lightBlack,
|
||||||
|
marginLeft: theme.spacing.unit,
|
||||||
|
marginRight: theme.spacing.unit,
|
||||||
|
},
|
||||||
|
|
||||||
|
input: {
|
||||||
|
lineHeight: 1,
|
||||||
|
paddingTop: 2,
|
||||||
|
paddingBottom: 2,
|
||||||
|
marginBottom: 1,
|
||||||
|
/*transition: theme.transitions.create('width', {
|
||||||
|
duration: theme.transitions.duration.shortest,
|
||||||
|
}),*/
|
||||||
|
minWidth: 130,
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
class SearchInput extends PureComponent {
|
||||||
|
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
value: props.defaultValue || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
this.input = null;
|
||||||
|
|
||||||
|
this.updateQuery = _debounce(this.updateQuery, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
if (!document) return;
|
||||||
|
const element = document.querySelector(`.search-input-${this.props.name} input[type=search]`);
|
||||||
|
|
||||||
|
element._addEventListener = element.addEventListener;
|
||||||
|
element.addEventListener = function(type, listener, useCapture) {
|
||||||
|
if(useCapture === undefined)
|
||||||
|
useCapture = false;
|
||||||
|
this._addEventListener(type, listener, useCapture);
|
||||||
|
};
|
||||||
|
|
||||||
|
element.addEventListener = element._addEventListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
}
|
||||||
|
|
||||||
|
handleShortcutKeys = (key, event) => {
|
||||||
|
switch (key) {
|
||||||
|
case 's':
|
||||||
|
this.focusInput();
|
||||||
|
event.preventDefault();
|
||||||
|
break;
|
||||||
|
case 'c':
|
||||||
|
case 'esc':
|
||||||
|
this.clearSearch(event, true);
|
||||||
|
event.preventDefault();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleFocus = () => {
|
||||||
|
this.input.select();
|
||||||
|
};
|
||||||
|
|
||||||
|
focusInput = (event) => {
|
||||||
|
this.input.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
clearSearch = (event, dontFocus) => {
|
||||||
|
this.setState({ value: '' });
|
||||||
|
this.updateQuery('');
|
||||||
|
|
||||||
|
if (!dontFocus) {
|
||||||
|
this.focusInput();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateSearch = (event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
this.setState({ value: value });
|
||||||
|
this.updateQuery(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateQuery = (value) => {
|
||||||
|
this.props.updateQuery(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const {
|
||||||
|
classes,
|
||||||
|
className,
|
||||||
|
dense,
|
||||||
|
noShortcuts,
|
||||||
|
name,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const searchIcon = <SearchIcon className={classes.icon} onClick={this.focusInput}/>;
|
||||||
|
|
||||||
|
const clearButton = <Components.TooltipIntl
|
||||||
|
titleId="search.clear"
|
||||||
|
icon={<ClearIcon/>}
|
||||||
|
onClick={this.clearSearch}
|
||||||
|
classes={{
|
||||||
|
root: classNames(!this.state.value && classes.clearDisabled),
|
||||||
|
button: classNames('clear-button', classes.clear, dense && classes.clearDense),
|
||||||
|
}}
|
||||||
|
disabled={!this.state.value}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<TextField
|
||||||
|
label="Search"
|
||||||
|
type="search"
|
||||||
|
id={`search-input-${name}`}
|
||||||
|
name={name}
|
||||||
|
title="Search"
|
||||||
|
value={this.state.value}
|
||||||
|
inputRef={input => this.input = input}
|
||||||
|
fullWidth
|
||||||
|
className={classNames('search-input', `search-input-${name}`, classes.root, dense && classes.inputTypeSearch, className, classes.textField)}
|
||||||
|
margin="normal"
|
||||||
|
variant="outlined"
|
||||||
|
onChange={this.updateSearch}
|
||||||
|
onFocus={this.handleFocus}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: searchIcon,
|
||||||
|
endAdornment: clearButton
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<NoSsr>
|
||||||
|
{
|
||||||
|
// KeyboardEventHandler is not valid on the server, where its name is undefined
|
||||||
|
typeof window !== 'undefined' && KeyboardEventHandler.name && !noShortcuts &&
|
||||||
|
|
||||||
|
<KeyboardEventHandler handleKeys={['s', 'c', 'esc']} onKeyEvent={this.handleShortcutKeys}/>
|
||||||
|
}
|
||||||
|
</NoSsr>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SearchInput.propTypes = {
|
||||||
|
classes: PropTypes.object.isRequired,
|
||||||
|
updateQuery: PropTypes.func.isRequired,
|
||||||
|
className: PropTypes.string,
|
||||||
|
dense: PropTypes.bool,
|
||||||
|
noShortcuts: PropTypes.bool,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
SearchInput.defaultProps = {
|
||||||
|
name: 'search',
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
SearchInput.displayName = 'SearchInput';
|
||||||
|
|
||||||
|
|
||||||
|
registerComponent('SearchInput', SearchInput, [withStyles, styles]);
|
|
@ -0,0 +1,108 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Components, registerComponent, Utils } from 'meteor/vulcan:core';
|
||||||
|
import { intlShape } from 'meteor/vulcan:i18n';
|
||||||
|
import withStyles from '@material-ui/core/styles/withStyles';
|
||||||
|
import withTheme from '@material-ui/core/styles/withTheme';
|
||||||
|
import Tooltip from '@material-ui/core/Tooltip';
|
||||||
|
import IconButton from '@material-ui/core/IconButton';
|
||||||
|
import Button from '@material-ui/core/Button';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
|
||||||
|
const styles = theme => ({
|
||||||
|
root: {},
|
||||||
|
tooltip: {
|
||||||
|
margin: '4px !important',
|
||||||
|
},
|
||||||
|
buttonWrap: {
|
||||||
|
display: 'inline-block',
|
||||||
|
},
|
||||||
|
button: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const TooltipIconButton = (props, { intl }) => {
|
||||||
|
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
titleId,
|
||||||
|
placement,
|
||||||
|
icon,
|
||||||
|
className,
|
||||||
|
classes,
|
||||||
|
theme,
|
||||||
|
buttonRef,
|
||||||
|
variant,
|
||||||
|
...properties
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const titleText = props.title || intl.formatMessage({ id: titleId });
|
||||||
|
const slug = Utils.slugify(titleId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip classes={{ tooltip: classNames('tooltip-icon-button', classes.tooltip, className) }}
|
||||||
|
id={`tooltip-${slug}`}
|
||||||
|
title={titleText}
|
||||||
|
placement={placement}
|
||||||
|
enterDelay={theme.utils.tooltipEnterDelay}
|
||||||
|
>
|
||||||
|
<div className={classes.buttonWrap}>
|
||||||
|
{
|
||||||
|
variant === 'fab'
|
||||||
|
|
||||||
|
?
|
||||||
|
|
||||||
|
<Button className={classNames(classes.button, slug)}
|
||||||
|
variant="fab"
|
||||||
|
aria-label={title}
|
||||||
|
ref={buttonRef}
|
||||||
|
{...properties}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
:
|
||||||
|
|
||||||
|
<IconButton className={classNames(classes.button, slug)}
|
||||||
|
aria-label={title}
|
||||||
|
ref={buttonRef}
|
||||||
|
{...properties}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
TooltipIconButton.propTypes = {
|
||||||
|
title: PropTypes.node,
|
||||||
|
titleId: PropTypes.string,
|
||||||
|
placement: PropTypes.string,
|
||||||
|
icon: PropTypes.node.isRequired,
|
||||||
|
className: PropTypes.string,
|
||||||
|
classes: PropTypes.object,
|
||||||
|
buttonRef: PropTypes.func,
|
||||||
|
variant: PropTypes.string,
|
||||||
|
theme: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
TooltipIconButton.defaultProps = {
|
||||||
|
placement: 'bottom',
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
TooltipIconButton.contextTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
TooltipIconButton.displayName = 'TooltipIconButton';
|
||||||
|
|
||||||
|
|
||||||
|
registerComponent('TooltipIconButton', TooltipIconButton, [withStyles, styles], [withTheme]);
|
168
packages/vulcan-ui-material/lib/components/bonus/TooltipIntl.jsx
Normal file
168
packages/vulcan-ui-material/lib/components/bonus/TooltipIntl.jsx
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Components, registerComponent, Utils } from 'meteor/vulcan:core';
|
||||||
|
import { intlShape } from 'meteor/vulcan:i18n';
|
||||||
|
import withStyles from '@material-ui/core/styles/withStyles';
|
||||||
|
import withTheme from '@material-ui/core/styles/withTheme';
|
||||||
|
import Tooltip from '@material-ui/core/Tooltip';
|
||||||
|
import IconButton from '@material-ui/core/IconButton';
|
||||||
|
import Button from '@material-ui/core/Button';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
|
||||||
|
const styles = theme => ({
|
||||||
|
|
||||||
|
root: {
|
||||||
|
display: 'inherit',
|
||||||
|
},
|
||||||
|
|
||||||
|
tooltip: {
|
||||||
|
margin: '4px !important',
|
||||||
|
},
|
||||||
|
|
||||||
|
buttonWrap: {
|
||||||
|
display: 'inherit',
|
||||||
|
},
|
||||||
|
|
||||||
|
button: {},
|
||||||
|
|
||||||
|
icon: {},
|
||||||
|
|
||||||
|
popoverPopper: {
|
||||||
|
zIndex: 1700,
|
||||||
|
},
|
||||||
|
|
||||||
|
popoverTooltip: {
|
||||||
|
zIndex: 1701,
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const TooltipIntl = (props, { intl }) => {
|
||||||
|
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
titleId,
|
||||||
|
titleValues,
|
||||||
|
placement,
|
||||||
|
icon,
|
||||||
|
className,
|
||||||
|
classes,
|
||||||
|
theme,
|
||||||
|
enterDelay,
|
||||||
|
leaveDelay,
|
||||||
|
buttonRef,
|
||||||
|
variant,
|
||||||
|
parent,
|
||||||
|
children,
|
||||||
|
...properties
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const iconWithClass = icon && React.cloneElement(icon, { className: classes.icon });
|
||||||
|
const popperClass = parent === 'popover' && classes.popoverPopper;
|
||||||
|
const tooltipClass = parent === 'popover' && classes.popoverTooltip;
|
||||||
|
const tooltipEnterDelay = typeof enterDelay === 'number' ? enterDelay : theme.utils.tooltipEnterDelay;
|
||||||
|
const tooltipLeaveDelay = typeof leaveDelay === 'number' ? leaveDelay : theme.utils.tooltipLeaveDelay;
|
||||||
|
const titleText = props.title || intl.formatMessage({ id: titleId }, titleValues);
|
||||||
|
const slug = Utils.slugify(titleId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={classNames('tooltip-intl', classes.root, className)}>
|
||||||
|
<Tooltip id={`tooltip-${slug}`}
|
||||||
|
title={titleText}
|
||||||
|
placement={placement}
|
||||||
|
enterDelay={tooltipEnterDelay}
|
||||||
|
leaveDelay={tooltipLeaveDelay}
|
||||||
|
classes={{
|
||||||
|
tooltip: classNames(classes.tooltip, tooltipClass),
|
||||||
|
popper: popperClass,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className={classes.buttonWrap}>
|
||||||
|
{
|
||||||
|
variant === 'fab' && !!icon
|
||||||
|
|
||||||
|
?
|
||||||
|
|
||||||
|
<Button className={classNames(classes.button, slug)}
|
||||||
|
variant="fab"
|
||||||
|
aria-label={title}
|
||||||
|
ref={buttonRef}
|
||||||
|
{...properties}
|
||||||
|
>
|
||||||
|
{iconWithClass}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
:
|
||||||
|
|
||||||
|
!!icon
|
||||||
|
|
||||||
|
?
|
||||||
|
|
||||||
|
<IconButton className={classNames(classes.button, slug)}
|
||||||
|
aria-label={title}
|
||||||
|
ref={buttonRef}
|
||||||
|
{...properties}
|
||||||
|
>
|
||||||
|
{iconWithClass}
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
:
|
||||||
|
|
||||||
|
variant === 'button'
|
||||||
|
|
||||||
|
?
|
||||||
|
<Button className={classNames(classes.button, slug)}
|
||||||
|
aria-label={title}
|
||||||
|
ref={buttonRef}
|
||||||
|
{...properties}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
:
|
||||||
|
|
||||||
|
children
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
TooltipIntl.propTypes = {
|
||||||
|
title: PropTypes.node,
|
||||||
|
titleId: PropTypes.string,
|
||||||
|
titleValues: PropTypes.object,
|
||||||
|
placement: PropTypes.string,
|
||||||
|
icon: PropTypes.node,
|
||||||
|
className: PropTypes.string,
|
||||||
|
classes: PropTypes.object,
|
||||||
|
buttonRef: PropTypes.func,
|
||||||
|
variant: PropTypes.string,
|
||||||
|
theme: PropTypes.object,
|
||||||
|
enterDelay: PropTypes.number,
|
||||||
|
leaveDelay: PropTypes.number,
|
||||||
|
parent: PropTypes.oneOf(['default', 'popover']),
|
||||||
|
children: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
TooltipIntl.defaultProps = {
|
||||||
|
placement: 'bottom',
|
||||||
|
parent: 'default',
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
TooltipIntl.contextTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
TooltipIntl.displayName = 'TooltipIntl';
|
||||||
|
|
||||||
|
|
||||||
|
registerComponent('TooltipIntl', TooltipIntl, [withStyles, styles], [withTheme]);
|
212
packages/vulcan-ui-material/lib/components/core/Card.jsx
Normal file
212
packages/vulcan-ui-material/lib/components/core/Card.jsx
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { intlShape } from 'meteor/vulcan:i18n';
|
||||||
|
import { replaceComponent, Components } from 'meteor/vulcan:core';
|
||||||
|
import moment from 'moment';
|
||||||
|
import withStyles from '@material-ui/core/styles/withStyles';
|
||||||
|
import IconButton from '@material-ui/core/IconButton';
|
||||||
|
import Checkbox from '@material-ui/core/Checkbox';
|
||||||
|
import EditIcon from 'mdi-material-ui/Pencil';
|
||||||
|
import Table from '@material-ui/core/Table';
|
||||||
|
import TableBody from '@material-ui/core/TableBody';
|
||||||
|
import TableRow from '@material-ui/core/TableRow';
|
||||||
|
import TableCell from '@material-ui/core/TableCell';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
|
||||||
|
const getLabel = (field, fieldName, collection, intl) => {
|
||||||
|
const schema = collection.simpleSchema()._schema;
|
||||||
|
const fieldSchema = schema[fieldName];
|
||||||
|
if (fieldSchema) {
|
||||||
|
return intl.formatMessage(
|
||||||
|
{ id: `${collection._name}.${fieldName}`, defaultMessage: fieldSchema.label });
|
||||||
|
} else {
|
||||||
|
return fieldName;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const getTypeName = (field, fieldName, collection) => {
|
||||||
|
const schema = collection.simpleSchema()._schema;
|
||||||
|
const fieldSchema = schema[fieldName];
|
||||||
|
if (fieldSchema) {
|
||||||
|
const type = fieldSchema.type.singleType;
|
||||||
|
const typeName = typeof type === 'function' ? type.name : type;
|
||||||
|
return typeName;
|
||||||
|
} else {
|
||||||
|
return typeof field;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const parseImageUrl = value => {
|
||||||
|
const isImage = ['.png', '.jpg', '.gif'].indexOf(value.substr(-4)) !== -1 ||
|
||||||
|
['.webp', '.jpeg'].indexOf(value.substr(-5)) !== -1;
|
||||||
|
return isImage ?
|
||||||
|
<img style={{ width: '100%', maxWidth: 200 }} src={value} alt={value}/> :
|
||||||
|
<LimitedString string={value}/>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const LimitedString = ({ string }) =>
|
||||||
|
<div>
|
||||||
|
{string.indexOf(' ') === -1 && string.length > 30 ?
|
||||||
|
<span title={string}>{string.substr(0, 30)}…</span> :
|
||||||
|
<span>{string}</span>
|
||||||
|
}
|
||||||
|
</div>;
|
||||||
|
|
||||||
|
|
||||||
|
export const getFieldValue = (value, typeName, classes={}) => {
|
||||||
|
|
||||||
|
if (typeof value === 'undefined' || value === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
typeName = 'Array';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof typeName === 'undefined') {
|
||||||
|
typeName = typeof value;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (typeName) {
|
||||||
|
|
||||||
|
case 'Boolean':
|
||||||
|
case 'boolean':
|
||||||
|
return <Checkbox checked={value} disabled style={{ width: '32px', height: '32px' }}/>;
|
||||||
|
|
||||||
|
case 'Number':
|
||||||
|
case 'number':
|
||||||
|
case 'SimpleSchema.Integer':
|
||||||
|
return <code>{value.toString()}</code>;
|
||||||
|
|
||||||
|
case 'Array':
|
||||||
|
return <ol>{value.map(
|
||||||
|
(item, index) => <li key={index}>{getFieldValue(item, typeof item, classes)}</li>)}</ol>;
|
||||||
|
|
||||||
|
case 'Object':
|
||||||
|
case 'object':
|
||||||
|
return (
|
||||||
|
<Table className="table">
|
||||||
|
<TableBody>
|
||||||
|
{_.map(value, (value, key) =>
|
||||||
|
<TableRow className={classNames(classes.table, 'table')} key={key}>
|
||||||
|
<TableCell className={classNames(classes.tableHeadCell, 'datacard-label')} variant="head">{key}</TableCell>
|
||||||
|
<TableCell className={classNames(classes.tableCell, 'datacard-value')} >{getFieldValue(value, typeof value, classes)}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'Date':
|
||||||
|
return moment(new Date(value)).format('dddd, MMMM Do YYYY, h:mm:ss');
|
||||||
|
|
||||||
|
default:
|
||||||
|
return parseImageUrl(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const CardItem = ({ label, value, typeName, classes }) =>
|
||||||
|
<TableRow className={classes.tableRow}>
|
||||||
|
<TableCell className={classNames(classes.tableHeadCell, 'datacard-label')} variant="head">
|
||||||
|
{label}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className={classNames(classes.tableCell, 'datacard-value')}>
|
||||||
|
{getFieldValue(value, typeName, classes)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>;
|
||||||
|
|
||||||
|
|
||||||
|
const CardEdit = (props, context) => {
|
||||||
|
const classes = props.classes;
|
||||||
|
const editTitle = context.intl.formatMessage({ id: 'cards.edit' });
|
||||||
|
return (
|
||||||
|
<TableRow className={classes.tableRow}>
|
||||||
|
<TableCell className={classes.tableCell} colSpan="2">
|
||||||
|
<Components.ModalTrigger label={editTitle}
|
||||||
|
component={<IconButton aria-label={editTitle}>
|
||||||
|
<EditIcon/>
|
||||||
|
</IconButton>}
|
||||||
|
>
|
||||||
|
<CardEditForm {...props} />
|
||||||
|
</Components.ModalTrigger>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
CardEdit.contextTypes = { intl: intlShape };
|
||||||
|
|
||||||
|
|
||||||
|
const CardEditForm = ({ collection, document, closeModal }) =>
|
||||||
|
<Components.SmartForm
|
||||||
|
collection={collection}
|
||||||
|
documentId={document._id}
|
||||||
|
showRemove={true}
|
||||||
|
successCallback={document => {
|
||||||
|
closeModal();
|
||||||
|
}}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
|
||||||
|
const styles = theme => ({
|
||||||
|
root: {},
|
||||||
|
table: {
|
||||||
|
maxWidth: '100%'
|
||||||
|
},
|
||||||
|
tableBody: {},
|
||||||
|
tableRow: {},
|
||||||
|
tableCell: {},
|
||||||
|
tableHeadCell: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const Card = ({ className, collection, document, currentUser, fields, classes }, { intl }) => {
|
||||||
|
|
||||||
|
const fieldNames = fields ? fields : _.without(_.keys(document), '__typename');
|
||||||
|
const canUpdate = currentUser && collection.options.mutations.update.check(currentUser, document);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames(classes.root, 'datacard', `datacard-${collection._name}`, className)}>
|
||||||
|
<Table className={classNames(classes.table, 'table')} style={{ maxWidth: '100%' }}>
|
||||||
|
<TableBody>
|
||||||
|
{canUpdate ? <CardEdit collection={collection} document={document} classes={classes}/> : null}
|
||||||
|
{fieldNames.map((fieldName, index) =>
|
||||||
|
<CardItem key={index}
|
||||||
|
value={document[fieldName]}
|
||||||
|
typeName={getTypeName(document[fieldName], fieldName, collection)}
|
||||||
|
label={getLabel(document[fieldName], fieldName, collection, intl)}
|
||||||
|
classes={classes}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Card.displayName = 'Card';
|
||||||
|
|
||||||
|
|
||||||
|
Card.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
collection: PropTypes.object,
|
||||||
|
document: PropTypes.object,
|
||||||
|
currentUser: PropTypes.object,
|
||||||
|
fields: PropTypes.array,
|
||||||
|
classes: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Card.contextTypes = {
|
||||||
|
intl: intlShape
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
replaceComponent('Card', Card, [withStyles, styles]);
|
649
packages/vulcan-ui-material/lib/components/core/Datatable.jsx
Normal file
649
packages/vulcan-ui-material/lib/components/core/Datatable.jsx
Normal file
|
@ -0,0 +1,649 @@
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
Components,
|
||||||
|
registerComponent,
|
||||||
|
replaceComponent,
|
||||||
|
withCurrentUser,
|
||||||
|
withMulti,
|
||||||
|
Utils
|
||||||
|
} from 'meteor/vulcan:core';
|
||||||
|
import { intlShape } from 'meteor/vulcan:i18n';
|
||||||
|
import withStyles from '@material-ui/core/styles/withStyles';
|
||||||
|
import Table from '@material-ui/core/Table';
|
||||||
|
import TableBody from '@material-ui/core/TableBody';
|
||||||
|
import TableHead from '@material-ui/core/TableHead';
|
||||||
|
import TableRow from '@material-ui/core/TableRow';
|
||||||
|
import TableCell from '@material-ui/core/TableCell';
|
||||||
|
import TableFooter from '@material-ui/core/TableFooter';
|
||||||
|
import Tooltip from '@material-ui/core/Tooltip';
|
||||||
|
import TableSortLabel from '@material-ui/core/TableSortLabel';
|
||||||
|
import TablePagination from '@material-ui/core/TablePagination';
|
||||||
|
import Toolbar from '@material-ui/core/Toolbar';
|
||||||
|
import Typography from '@material-ui/core/Typography';
|
||||||
|
import { getFieldValue } from './Card';
|
||||||
|
import _assign from 'lodash/assign';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Datatable Component
|
||||||
|
|
||||||
|
*/
|
||||||
|
const baseStyles = theme => ({
|
||||||
|
root: {
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
addButton: {
|
||||||
|
top: '9.5rem',
|
||||||
|
right: '2rem',
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: 'auto',
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
marginTop:0
|
||||||
|
},
|
||||||
|
denseTable: {},
|
||||||
|
denserTable: {},
|
||||||
|
flatTable: {},
|
||||||
|
tableHead: {},
|
||||||
|
tableBody: {},
|
||||||
|
tableFooter: {},
|
||||||
|
tableRow: {},
|
||||||
|
tableHeadCell: {},
|
||||||
|
tableCell: {},
|
||||||
|
clickRow: {},
|
||||||
|
editCell: {},
|
||||||
|
editButton: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const delay = (function () {
|
||||||
|
var timer = 0;
|
||||||
|
return function (callback, ms) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(callback, ms);
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
class Datatable extends PureComponent {
|
||||||
|
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.updateQuery = this.updateQuery.bind(this);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
value: '',
|
||||||
|
query: '',
|
||||||
|
currentSort: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSort = column => {
|
||||||
|
let currentSort;
|
||||||
|
if (!this.state.currentSort[column]) {
|
||||||
|
currentSort = { [column]: 1 };
|
||||||
|
} else if (this.state.currentSort[column] === 1) {
|
||||||
|
currentSort = { [column]: -1 };
|
||||||
|
} else {
|
||||||
|
currentSort = {};
|
||||||
|
}
|
||||||
|
this.setState({ currentSort });
|
||||||
|
};
|
||||||
|
|
||||||
|
updateQuery (value) {
|
||||||
|
this.setState({
|
||||||
|
value: value
|
||||||
|
});
|
||||||
|
delay(() => {
|
||||||
|
this.setState({
|
||||||
|
query: value
|
||||||
|
});
|
||||||
|
}, 700);
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
if (this.props.data) {
|
||||||
|
|
||||||
|
return <Components.DatatableContents
|
||||||
|
columns={this.props.data.length ? Object.keys(this.props.data[0]) : undefined}
|
||||||
|
{...this.props}
|
||||||
|
results={this.props.data}
|
||||||
|
count={this.props.data.length}
|
||||||
|
totalCount={this.props.data.length}
|
||||||
|
showEdit={false}
|
||||||
|
showNew={false}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
collection,
|
||||||
|
options,
|
||||||
|
showSearch,
|
||||||
|
showNew,
|
||||||
|
currentUser,
|
||||||
|
classes,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const listOptions = {
|
||||||
|
collection: collection,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DatatableWithMulti = withMulti(listOptions)(Components.DatatableContents);
|
||||||
|
|
||||||
|
// add _id to orderBy when we want to sort a column, to avoid breaking the graphql() hoc;
|
||||||
|
// see https://github.com/VulcanJS/Vulcan/issues/2090#issuecomment-433860782
|
||||||
|
// this.state.currentSort !== {} is always false, even when console.log(this.state.currentSort) displays
|
||||||
|
// {}. So we test on the length of keys for this object.
|
||||||
|
const orderBy = Object.keys(this.state.currentSort).length == 0 ? {} :
|
||||||
|
{ ...this.state.currentSort, _id: -1 };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames('datatable', `datatable-${collection._name}`, classes.root,
|
||||||
|
className)}>
|
||||||
|
{/* DatatableAbove Component part*/}
|
||||||
|
{
|
||||||
|
showSearch &&
|
||||||
|
|
||||||
|
<Components.SearchInput value={this.state.query}
|
||||||
|
updateQuery={this.updateQuery}
|
||||||
|
className={classes.search}
|
||||||
|
labelId={'datatable.search'}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
showNew &&
|
||||||
|
|
||||||
|
<Components.NewButton collection={collection}
|
||||||
|
variant="fab"
|
||||||
|
color="primary"
|
||||||
|
className={classes.addButton}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<DatatableWithMulti {...this.props}
|
||||||
|
collection={collection}
|
||||||
|
terms={{ query: this.state.query, orderBy: orderBy }}
|
||||||
|
currentUser={this.props.currentUser}
|
||||||
|
toggleSort={this.toggleSort}
|
||||||
|
currentSort={this.state.currentSort}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Datatable.propTypes = {
|
||||||
|
title: PropTypes.string,
|
||||||
|
className: PropTypes.string,
|
||||||
|
collection: PropTypes.object,
|
||||||
|
options: PropTypes.object,
|
||||||
|
columns: PropTypes.array,
|
||||||
|
showEdit: PropTypes.bool,
|
||||||
|
editComponent: PropTypes.func,
|
||||||
|
showNew: PropTypes.bool,
|
||||||
|
showSearch: PropTypes.bool,
|
||||||
|
emptyState: PropTypes.node,
|
||||||
|
currentUser: PropTypes.object,
|
||||||
|
classes: PropTypes.object,
|
||||||
|
data: PropTypes.array,
|
||||||
|
footerData: PropTypes.array,
|
||||||
|
dense: PropTypes.string,
|
||||||
|
queryDataRef: PropTypes.func,
|
||||||
|
rowClass: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
||||||
|
handleRowClick: PropTypes.func,
|
||||||
|
intlNamespace: PropTypes.string,
|
||||||
|
toggleSort: PropTypes.func,
|
||||||
|
currentSort: PropTypes.object,
|
||||||
|
paginate: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Datatable.defaultProps = {
|
||||||
|
showNew: true,
|
||||||
|
showEdit: true,
|
||||||
|
showSearch: true,
|
||||||
|
paginate: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
replaceComponent('Datatable', Datatable, withCurrentUser, [withStyles, baseStyles]);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
DatatableContents Component
|
||||||
|
|
||||||
|
*/
|
||||||
|
const datatableContentsStyles = theme => (_assign({}, baseStyles(theme), {
|
||||||
|
table: {
|
||||||
|
marginTop: theme.spacing.unit * 3,
|
||||||
|
marginBottom: theme.spacing.unit * 3,
|
||||||
|
},
|
||||||
|
denseTable: theme.utils.denseTable,
|
||||||
|
flatTable: theme.utils.flatTable,
|
||||||
|
denserTable: theme.utils.denserTable,
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
const DatatableContents = ({
|
||||||
|
collection,
|
||||||
|
columns,
|
||||||
|
results,
|
||||||
|
loading,
|
||||||
|
loadMore,
|
||||||
|
count,
|
||||||
|
totalCount,
|
||||||
|
networkStatus,
|
||||||
|
refetch,
|
||||||
|
showEdit,
|
||||||
|
editComponent,
|
||||||
|
emptyState,
|
||||||
|
currentUser,
|
||||||
|
classes,
|
||||||
|
footerData,
|
||||||
|
dense,
|
||||||
|
queryDataRef,
|
||||||
|
rowClass,
|
||||||
|
handleRowClick,
|
||||||
|
intlNamespace,
|
||||||
|
title,
|
||||||
|
toggleSort,
|
||||||
|
currentSort,
|
||||||
|
paginate,
|
||||||
|
paginationTerms,
|
||||||
|
setPaginationTerms
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Components.Loading/>;
|
||||||
|
} else if (!results || !results.length) {
|
||||||
|
return emptyState || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryDataRef) queryDataRef(this.props);
|
||||||
|
|
||||||
|
const denseClass = dense && classes[dense + 'Table'];
|
||||||
|
|
||||||
|
// Pagination functions
|
||||||
|
const getPage = (paginationTerms) => (parseInt((paginationTerms.limit - 1) / paginationTerms.itemsPerPage));
|
||||||
|
|
||||||
|
const onChangePage = (event, page) => {
|
||||||
|
setPaginationTerms({
|
||||||
|
itemsPerPage: paginationTerms.itemsPerPage,
|
||||||
|
limit: (page + 1) * paginationTerms.itemsPerPage,
|
||||||
|
offset: page * paginationTerms.itemsPerPage
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChangeRowsPerPage = (event) => {
|
||||||
|
let value = event.target.value;
|
||||||
|
let offset = Math.max(0, parseInt((paginationTerms.limit - paginationTerms.itemsPerPage) / value) * value);
|
||||||
|
let limit = Math.min(offset + value, totalCount);
|
||||||
|
setPaginationTerms({
|
||||||
|
itemsPerPage: value,
|
||||||
|
limit: limit,
|
||||||
|
offset: offset
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{
|
||||||
|
(title)?
|
||||||
|
<Toolbar>
|
||||||
|
<Typography variant="h6" id="tableTitle">
|
||||||
|
title
|
||||||
|
</Typography>
|
||||||
|
</Toolbar>
|
||||||
|
:null
|
||||||
|
}
|
||||||
|
<Table className={classNames(classes.table, denseClass)}>
|
||||||
|
{
|
||||||
|
columns &&
|
||||||
|
<TableHead className={classes.tableHead}>
|
||||||
|
<TableRow className={classes.tableRow}>
|
||||||
|
{
|
||||||
|
_.sortBy(columns, column => column.order).map(
|
||||||
|
(column, index) =>
|
||||||
|
<Components.DatatableHeader key={index}
|
||||||
|
collection={collection}
|
||||||
|
intlNamespace={intlNamespace}
|
||||||
|
column={column}
|
||||||
|
classes={classes}
|
||||||
|
toggleSort={toggleSort}
|
||||||
|
currentSort={currentSort}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
(showEdit || editComponent) &&
|
||||||
|
|
||||||
|
<TableCell className={classes.tableCell}/>
|
||||||
|
}
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
results &&
|
||||||
|
|
||||||
|
<TableBody className={classes.tableBody}>
|
||||||
|
{
|
||||||
|
results.map(
|
||||||
|
(document, index) =>
|
||||||
|
<Components.DatatableRow collection={collection}
|
||||||
|
columns={columns}
|
||||||
|
document={document}
|
||||||
|
refetch={refetch}
|
||||||
|
key={index}
|
||||||
|
showEdit={showEdit}
|
||||||
|
editComponent={editComponent}
|
||||||
|
currentUser={currentUser}
|
||||||
|
classes={classes}
|
||||||
|
rowClass={rowClass}
|
||||||
|
handleRowClick={handleRowClick}
|
||||||
|
/>)
|
||||||
|
}
|
||||||
|
</TableBody>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
footerData &&
|
||||||
|
|
||||||
|
<TableFooter className={classes.tableFooter}>
|
||||||
|
<TableRow className={classes.tableRow}>
|
||||||
|
{
|
||||||
|
_.sortBy(columns, column => column.order).map(
|
||||||
|
(column, index) =>
|
||||||
|
<TableCell key={index} className={classNames(classes.tableCell, column.footerClass)}>
|
||||||
|
{footerData[index]}
|
||||||
|
</TableCell>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
(showEdit || editComponent) &&
|
||||||
|
|
||||||
|
<TableCell className={classes.tableCell}/>
|
||||||
|
}
|
||||||
|
</TableRow>
|
||||||
|
</TableFooter>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</Table>
|
||||||
|
{
|
||||||
|
paginate &&
|
||||||
|
|
||||||
|
<TablePagination
|
||||||
|
component="div"
|
||||||
|
count={totalCount}
|
||||||
|
rowsPerPage={paginationTerms.itemsPerPage}
|
||||||
|
page={getPage(paginationTerms)}
|
||||||
|
backIconButtonProps={{
|
||||||
|
'aria-label': 'Previous Page',
|
||||||
|
}}
|
||||||
|
nextIconButtonProps={{
|
||||||
|
'aria-label': 'Next Page',
|
||||||
|
}}
|
||||||
|
onChangePage={onChangePage}
|
||||||
|
onChangeRowsPerPage={onChangeRowsPerPage}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
!paginate && loadMore &&
|
||||||
|
|
||||||
|
<Components.LoadMore className={classes.loadMore}
|
||||||
|
count={count}
|
||||||
|
totalCount={totalCount}
|
||||||
|
loadMore={loadMore}
|
||||||
|
networkStatus={networkStatus}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
replaceComponent('DatatableContents', DatatableContents, [withStyles, datatableContentsStyles]);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
DatatableHeader Component
|
||||||
|
|
||||||
|
*/
|
||||||
|
const DatatableHeader = ({ collection, intlNamespace, column, classes, toggleSort, currentSort }, { intl }) => {
|
||||||
|
const columnName = typeof column === 'string' ? column : column.name || column.label;
|
||||||
|
let formattedLabel = '';
|
||||||
|
|
||||||
|
if (collection) {
|
||||||
|
const schema = collection.simpleSchema()._schema;
|
||||||
|
|
||||||
|
/*
|
||||||
|
use either:
|
||||||
|
|
||||||
|
1. the column name translation
|
||||||
|
2. the column name label in the schema (if the column name matches a schema field)
|
||||||
|
3. the raw column name.
|
||||||
|
*/
|
||||||
|
const defaultMessage = schema[columnName] ? schema[columnName].label : Utils.camelToSpaces(columnName);
|
||||||
|
formattedLabel = typeof columnName === 'string' ?
|
||||||
|
intl.formatMessage({
|
||||||
|
id: `${collection._name}.${columnName}`,
|
||||||
|
defaultMessage: defaultMessage
|
||||||
|
}) :
|
||||||
|
'';
|
||||||
|
|
||||||
|
// if sortable is a string, use it as the name of the property to sort by. If it's just `true`, use
|
||||||
|
// column.name
|
||||||
|
const sortPropertyName = typeof column.sortable === 'string' ? column.sortable : column.name;
|
||||||
|
|
||||||
|
if (column.sortable) {
|
||||||
|
return <Components.DatatableSorter name={sortPropertyName}
|
||||||
|
label={formattedLabel}
|
||||||
|
toggleSort={toggleSort}
|
||||||
|
currentSort={currentSort}
|
||||||
|
sortable={column.sortable}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
} else if (intlNamespace) {
|
||||||
|
formattedLabel = typeof columnName === 'string' ?
|
||||||
|
intl.formatMessage({
|
||||||
|
id: `${intlNamespace}.${columnName}`,
|
||||||
|
defaultMessage: columnName
|
||||||
|
}) :
|
||||||
|
'';
|
||||||
|
} else {
|
||||||
|
formattedLabel = intl.formatMessage({ id: columnName, defaultMessage: columnName });
|
||||||
|
}
|
||||||
|
|
||||||
|
return <TableCell
|
||||||
|
className={classNames(classes.tableHeadCell, column.headerClass)}>{formattedLabel}</TableCell>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
DatatableHeader.contextTypes = {
|
||||||
|
intl: intlShape,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
replaceComponent('DatatableHeader', DatatableHeader);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
DatatableSorter Component
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
const DatatableSorter = ({ name, label, toggleSort, currentSort, sortable }) =>
|
||||||
|
<TableCell className="datatable-sorter"
|
||||||
|
sortDirection={!currentSort[name] ? false : currentSort[name] === 1 ? 'asc' : 'desc'}
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
title="Sort"
|
||||||
|
placement='bottom-start'
|
||||||
|
enterDelay={300}
|
||||||
|
>
|
||||||
|
<TableSortLabel
|
||||||
|
active={!currentSort[name] ? false : true}
|
||||||
|
direction={currentSort[name] === 1 ? 'desc' : 'asc'}
|
||||||
|
onClick={() => toggleSort(name)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</TableSortLabel>
|
||||||
|
</Tooltip>
|
||||||
|
</TableCell>;
|
||||||
|
|
||||||
|
replaceComponent('DatatableSorter', DatatableSorter);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
DatatableRow Component
|
||||||
|
|
||||||
|
*/
|
||||||
|
const datatableRowStyles = theme => (_assign({}, baseStyles(theme), {
|
||||||
|
clickRow: {
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
editCell: {
|
||||||
|
paddingTop: '0 !important',
|
||||||
|
paddingBottom: '0 !important',
|
||||||
|
textAlign: 'right',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
const DatatableRow = ({
|
||||||
|
collection,
|
||||||
|
columns,
|
||||||
|
document,
|
||||||
|
refetch,
|
||||||
|
showEdit,
|
||||||
|
editComponent,
|
||||||
|
currentUser,
|
||||||
|
rowClass,
|
||||||
|
handleRowClick,
|
||||||
|
classes,
|
||||||
|
}, { intl }) => {
|
||||||
|
|
||||||
|
const EditComponent = editComponent;
|
||||||
|
|
||||||
|
if (typeof rowClass === 'function') {
|
||||||
|
rowClass = rowClass(document);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow
|
||||||
|
className={classNames('datatable-item', classes.tableRow, rowClass, handleRowClick && classes.clickRow)}
|
||||||
|
onClick={handleRowClick && (event => handleRowClick(event, document))}
|
||||||
|
hover
|
||||||
|
>
|
||||||
|
|
||||||
|
{
|
||||||
|
_.sortBy(columns, column => column.order).map(
|
||||||
|
(column, index) =>
|
||||||
|
<Components.DatatableCell key={index}
|
||||||
|
column={column}
|
||||||
|
document={document}
|
||||||
|
currentUser={currentUser}
|
||||||
|
classes={classes}
|
||||||
|
/>)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
(showEdit || editComponent) &&
|
||||||
|
|
||||||
|
<TableCell className={classes.editCell}>
|
||||||
|
{
|
||||||
|
EditComponent &&
|
||||||
|
|
||||||
|
<EditComponent collection={collection} document={document} refetch={refetch}/>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
showEdit &&
|
||||||
|
|
||||||
|
<Components.EditButton collection={collection}
|
||||||
|
document={document}
|
||||||
|
buttonClasses={{ button: classes.editButton }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</TableCell>
|
||||||
|
}
|
||||||
|
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
replaceComponent('DatatableRow', DatatableRow, [withStyles, datatableRowStyles]);
|
||||||
|
|
||||||
|
|
||||||
|
DatatableRow.contextTypes = {
|
||||||
|
intl: intlShape
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
DatatableCell Component
|
||||||
|
|
||||||
|
*/
|
||||||
|
const DatatableCell = ({ column, document, currentUser, classes }) => {
|
||||||
|
const Component = column.component ||
|
||||||
|
Components[column.componentName] ||
|
||||||
|
Components.DatatableDefaultCell;
|
||||||
|
|
||||||
|
const columnName = typeof column === 'string' ? column : column.name;
|
||||||
|
const className = typeof columnName === 'string' ?
|
||||||
|
`datatable-item-${columnName.toLowerCase()}` :
|
||||||
|
'';
|
||||||
|
const cellClass = typeof column.cellClass === 'function' ?
|
||||||
|
column.cellClass({ column, document, currentUser }) :
|
||||||
|
typeof column.cellClass === 'string' ?
|
||||||
|
column.cellClass :
|
||||||
|
null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableCell className={classNames(classes.tableCell, cellClass, className)}>
|
||||||
|
<Component column={column}
|
||||||
|
document={document}
|
||||||
|
currentUser={currentUser}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
replaceComponent('DatatableCell', DatatableCell);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
DatatableDefaultCell Component
|
||||||
|
|
||||||
|
*/
|
||||||
|
const DatatableDefaultCell = ({ column, document }) =>
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
typeof column === 'string'
|
||||||
|
?
|
||||||
|
getFieldValue(document[column])
|
||||||
|
:
|
||||||
|
getFieldValue(document[column.name])
|
||||||
|
}
|
||||||
|
</div>;
|
||||||
|
|
||||||
|
|
||||||
|
replaceComponent('DatatableDefaultCell', DatatableDefaultCell);
|
100
packages/vulcan-ui-material/lib/components/core/EditButton.jsx
Normal file
100
packages/vulcan-ui-material/lib/components/core/EditButton.jsx
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Components, registerComponent } from 'meteor/vulcan:core';
|
||||||
|
import { intlShape } from 'meteor/vulcan:i18n';
|
||||||
|
import EditIcon from 'mdi-material-ui/Pencil';
|
||||||
|
|
||||||
|
const EditButton = (
|
||||||
|
{
|
||||||
|
collection,
|
||||||
|
document,
|
||||||
|
color = 'default',
|
||||||
|
variant,
|
||||||
|
triggerClasses,
|
||||||
|
buttonClasses,
|
||||||
|
showRemove,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
{ intl }
|
||||||
|
) => (
|
||||||
|
<Components.ModalTrigger
|
||||||
|
classes={triggerClasses}
|
||||||
|
component={
|
||||||
|
<Components.TooltipIconButton
|
||||||
|
titleId="datatable.edit"
|
||||||
|
icon={<EditIcon />}
|
||||||
|
color={color}
|
||||||
|
variant={variant}
|
||||||
|
classes={buttonClasses}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Components.EditForm
|
||||||
|
collection={collection}
|
||||||
|
document={document}
|
||||||
|
showRemove={showRemove}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</Components.ModalTrigger>
|
||||||
|
);
|
||||||
|
|
||||||
|
EditButton.propTypes = {
|
||||||
|
collection: PropTypes.object.isRequired,
|
||||||
|
document: PropTypes.object.isRequired,
|
||||||
|
color: PropTypes.oneOf(['default', 'inherit', 'primary', 'secondary']),
|
||||||
|
variant: PropTypes.string,
|
||||||
|
triggerClasses: PropTypes.object,
|
||||||
|
buttonClasses: PropTypes.object,
|
||||||
|
showRemove: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
EditButton.contextTypes = {
|
||||||
|
intl: intlShape
|
||||||
|
};
|
||||||
|
|
||||||
|
EditButton.displayName = 'EditButton';
|
||||||
|
|
||||||
|
registerComponent('EditButton', EditButton);
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
EditForm Component
|
||||||
|
|
||||||
|
*/
|
||||||
|
const EditForm = ({
|
||||||
|
collection,
|
||||||
|
document,
|
||||||
|
closeModal,
|
||||||
|
options,
|
||||||
|
successCallback,
|
||||||
|
removeSuccessCallback,
|
||||||
|
showRemove,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const success = successCallback
|
||||||
|
? () => {
|
||||||
|
successCallback();
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
: closeModal;
|
||||||
|
|
||||||
|
const remove = removeSuccessCallback
|
||||||
|
? () => {
|
||||||
|
removeSuccessCallback();
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
: closeModal;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Components.SmartForm
|
||||||
|
{...props}
|
||||||
|
collection={collection}
|
||||||
|
documentId={document && document._id}
|
||||||
|
showRemove={showRemove ? true : showRemove}
|
||||||
|
successCallback={success}
|
||||||
|
removeSuccessCallback={remove}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerComponent('EditForm', EditForm);
|
|
@ -0,0 +1,9 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { replaceComponent } from 'meteor/vulcan:core';
|
||||||
|
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||||
|
|
||||||
|
function Loading(props) {
|
||||||
|
return <CircularProgress {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceComponent('Loading', Loading);
|
159
packages/vulcan-ui-material/lib/components/core/ModalTrigger.jsx
Normal file
159
packages/vulcan-ui-material/lib/components/core/ModalTrigger.jsx
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { intlShape } from 'meteor/vulcan:i18n';
|
||||||
|
import { registerComponent, Components } from 'meteor/vulcan:core';
|
||||||
|
import withStyles from '@material-ui/core/styles/withStyles';
|
||||||
|
import Dialog from '@material-ui/core/Dialog';
|
||||||
|
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||||
|
import DialogContent from '@material-ui/core/DialogContent';
|
||||||
|
import Button from '@material-ui/core/Button';
|
||||||
|
import Tooltip from '@material-ui/core/Tooltip';
|
||||||
|
import Close from 'mdi-material-ui/Close';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
|
||||||
|
const styles = theme => ({
|
||||||
|
root: {
|
||||||
|
display: 'inline-block',
|
||||||
|
},
|
||||||
|
button: {},
|
||||||
|
anchor: {},
|
||||||
|
dialog: {},
|
||||||
|
dialogPaper: {
|
||||||
|
overflowY: 'visible',
|
||||||
|
},
|
||||||
|
dialogTitle: {
|
||||||
|
padding: theme.spacing.unit * 4,
|
||||||
|
},
|
||||||
|
dialogContent: {
|
||||||
|
paddingTop: '4px',
|
||||||
|
},
|
||||||
|
closeButton: {
|
||||||
|
position: 'absolute',
|
||||||
|
right: theme.spacing.unit,
|
||||||
|
top: theme.spacing.unit,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
class ModalTrigger extends PureComponent {
|
||||||
|
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { modalIsOpen: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (this.props.action) {
|
||||||
|
this.props.action({
|
||||||
|
openModal: this.openModal,
|
||||||
|
closeModal: this.closeModal,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openModal = () => {
|
||||||
|
this.setState({ modalIsOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
closeModal = () => {
|
||||||
|
this.setState({ modalIsOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
dialogClassName,
|
||||||
|
dialogOverflow,
|
||||||
|
labelId,
|
||||||
|
component,
|
||||||
|
titleId,
|
||||||
|
type,
|
||||||
|
children,
|
||||||
|
classes,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const intl = this.context.intl;
|
||||||
|
|
||||||
|
const label = labelId ? intl.formatMessage({ id: labelId }) : this.props.label;
|
||||||
|
const title = titleId ? intl.formatMessage({ id: titleId }) : this.props.title;
|
||||||
|
const overflowClass = dialogOverflow && classes.dialogOverflow;
|
||||||
|
|
||||||
|
const triggerComponent = component
|
||||||
|
?
|
||||||
|
React.cloneElement(component, { onClick: this.openModal })
|
||||||
|
:
|
||||||
|
type === 'button'
|
||||||
|
?
|
||||||
|
<Button className={classes.button} variant="contained" onClick={this.openModal}>{label}</Button>
|
||||||
|
:
|
||||||
|
<a className={classes.anchor} href="#" onClick={this.openModal}>{label}</a>;
|
||||||
|
|
||||||
|
const childrenComponent = typeof children.type === 'function' ?
|
||||||
|
React.cloneElement(children, { closeModal: this.closeModal }) :
|
||||||
|
children;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={classNames('modal-trigger', classes.root, className)}>
|
||||||
|
|
||||||
|
{triggerComponent}
|
||||||
|
<Dialog className={classNames(dialogClassName)}
|
||||||
|
open={this.state.modalIsOpen}
|
||||||
|
onClose={this.closeModal}
|
||||||
|
fullWidth={true}
|
||||||
|
classes={{ paper: classes.paper }}
|
||||||
|
>
|
||||||
|
<DialogTitle className={classes.dialogTitle}>
|
||||||
|
{title}
|
||||||
|
|
||||||
|
<Components.Button iconButton aria-label="Close" className={classes.closeButton} onClick={this.closeModal}>
|
||||||
|
<Tooltip title={intl.formatMessage({ id: 'modal.close' })}>
|
||||||
|
<Close />
|
||||||
|
</Tooltip>
|
||||||
|
</Components.Button>
|
||||||
|
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
<DialogContent className={classes.dialogContent}>
|
||||||
|
<Components.ErrorCatcher>
|
||||||
|
{childrenComponent}
|
||||||
|
</Components.ErrorCatcher>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ModalTrigger.propTypes = {
|
||||||
|
/**
|
||||||
|
* Callback fired when the component mounts.
|
||||||
|
* This is useful when you want to trigger an action programmatically.
|
||||||
|
* It supports `openModal()` and `closeModal()`.
|
||||||
|
*
|
||||||
|
* @param {object} actions This object contains all possible actions
|
||||||
|
* that can be triggered programmatically.
|
||||||
|
*/
|
||||||
|
action: PropTypes.func,
|
||||||
|
className: PropTypes.string,
|
||||||
|
dialogClassName: PropTypes.string,
|
||||||
|
dialogOverflow: PropTypes.bool,
|
||||||
|
label: PropTypes.string,
|
||||||
|
labelId: PropTypes.string,
|
||||||
|
component: PropTypes.object,
|
||||||
|
title: PropTypes.node,
|
||||||
|
titleId: PropTypes.string,
|
||||||
|
type: PropTypes.oneOf(['link', 'button']),
|
||||||
|
children: PropTypes.node,
|
||||||
|
classes: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
ModalTrigger.contextTypes = {
|
||||||
|
intl: intlShape,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
registerComponent('ModalTrigger', ModalTrigger, [withStyles, styles]);
|
|
@ -0,0 +1,44 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Components, replaceComponent } from 'meteor/vulcan:core';
|
||||||
|
import { intlShape } from 'meteor/vulcan:i18n';
|
||||||
|
import AddIcon from 'mdi-material-ui/Plus';
|
||||||
|
|
||||||
|
|
||||||
|
const NewButton = ({
|
||||||
|
className,
|
||||||
|
collection,
|
||||||
|
color = 'default',
|
||||||
|
variant,
|
||||||
|
}, { intl }) => (
|
||||||
|
|
||||||
|
<Components.ModalTrigger
|
||||||
|
className={className}
|
||||||
|
component={<Components.TooltipIconButton titleId="datatable.new"
|
||||||
|
icon={<AddIcon/>}
|
||||||
|
color={color}
|
||||||
|
variant={variant}
|
||||||
|
/>}
|
||||||
|
>
|
||||||
|
<Components.EditForm collection={collection}/>
|
||||||
|
</Components.ModalTrigger>
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
NewButton.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
collection: PropTypes.object.isRequired,
|
||||||
|
color: PropTypes.oneOf(['default', 'inherit', 'primary', 'secondary']),
|
||||||
|
variant: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
NewButton.contextTypes = {
|
||||||
|
intl: intlShape
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
NewButton.displayName = 'NewButton';
|
||||||
|
|
||||||
|
|
||||||
|
replaceComponent('NewButton', NewButton);
|
|
@ -0,0 +1,129 @@
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { intlShape } from 'meteor/vulcan:i18n';
|
||||||
|
import { Components, registerComponent, instantiateComponent } from 'meteor/vulcan:core';
|
||||||
|
import withStyles from '@material-ui/core/styles/withStyles';
|
||||||
|
import _omit from 'lodash/omit';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
|
||||||
|
const styles = theme => ({
|
||||||
|
|
||||||
|
formInput: {
|
||||||
|
position: 'relative',
|
||||||
|
marginBottom: theme.spacing.unit * 3,
|
||||||
|
},
|
||||||
|
|
||||||
|
halfWidthLeft: {
|
||||||
|
display: 'inline-block',
|
||||||
|
width: '48%',
|
||||||
|
verticalAlign: 'top',
|
||||||
|
marginRight: '4%',
|
||||||
|
},
|
||||||
|
|
||||||
|
halfWidthRight: {
|
||||||
|
display: 'inline-block',
|
||||||
|
width: '48%',
|
||||||
|
verticalAlign: 'top',
|
||||||
|
},
|
||||||
|
|
||||||
|
thirdWidthLeft: {
|
||||||
|
display: 'inline-block',
|
||||||
|
width: '31%',
|
||||||
|
verticalAlign: 'top',
|
||||||
|
marginRight: '3.5%',
|
||||||
|
},
|
||||||
|
|
||||||
|
thirdWidthRight: {
|
||||||
|
display: 'inline-block',
|
||||||
|
width: '31%',
|
||||||
|
verticalAlign: 'top',
|
||||||
|
},
|
||||||
|
|
||||||
|
hidden: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
class FormComponentInner extends PureComponent {
|
||||||
|
|
||||||
|
getProperties = () => {
|
||||||
|
return _omit(this.props, 'classes');
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const {
|
||||||
|
classes,
|
||||||
|
inputClassName,
|
||||||
|
name,
|
||||||
|
input,
|
||||||
|
hidden,
|
||||||
|
beforeComponent,
|
||||||
|
afterComponent,
|
||||||
|
formInput,
|
||||||
|
intlInput,
|
||||||
|
nestedInput,
|
||||||
|
formComponents,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const FormComponents = formComponents;
|
||||||
|
|
||||||
|
const inputClass = classNames(
|
||||||
|
classes.formInput,
|
||||||
|
hidden && classes.hidden,
|
||||||
|
inputClassName && classes[inputClassName],
|
||||||
|
`input-${name}`,
|
||||||
|
`form-component-${input || 'default'}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const properties = this.getProperties();
|
||||||
|
|
||||||
|
const FormInput = formInput;
|
||||||
|
|
||||||
|
if (intlInput) {
|
||||||
|
return <Components.FormIntl {...properties} />;
|
||||||
|
} else if (nestedInput){
|
||||||
|
return <Components.FormNested {...properties} />;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div className={inputClass}>
|
||||||
|
{instantiateComponent(beforeComponent, properties)}
|
||||||
|
<FormInput {...properties}/>
|
||||||
|
{instantiateComponent(afterComponent, properties)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
FormComponentInner.contextTypes = {
|
||||||
|
intl: intlShape,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
FormComponentInner.propTypes = {
|
||||||
|
classes: PropTypes.object.isRequired,
|
||||||
|
inputClassName: PropTypes.string,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
input: PropTypes.any,
|
||||||
|
beforeComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||||
|
afterComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||||
|
errors: PropTypes.array.isRequired,
|
||||||
|
help: PropTypes.node,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
showCharsRemaining: PropTypes.bool.isRequired,
|
||||||
|
charsRemaining: PropTypes.number,
|
||||||
|
charsCount: PropTypes.number,
|
||||||
|
max: PropTypes.oneOfType([PropTypes.number, PropTypes.instanceOf(Date)]),
|
||||||
|
formInput: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
FormComponentInner.displayName = 'FormComponentInner';
|
||||||
|
|
||||||
|
|
||||||
|
registerComponent('FormComponentInner', FormComponentInner, [withStyles, styles]);
|
|
@ -0,0 +1,51 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { replaceComponent, Components } from 'meteor/vulcan:core';
|
||||||
|
import { FormattedMessage } from 'meteor/vulcan:i18n';
|
||||||
|
|
||||||
|
import Snackbar from '@material-ui/core/Snackbar';
|
||||||
|
import withStyles from '@material-ui/core/styles/withStyles';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
|
||||||
|
const styles = theme => ({
|
||||||
|
root: {
|
||||||
|
position: 'relative',
|
||||||
|
boxShadow: 'none',
|
||||||
|
marginBottom: theme.spacing.unit * 2
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
marginBottom: 0
|
||||||
|
},
|
||||||
|
error: { '& > div': { backgroundColor: theme.palette.error[500] } },
|
||||||
|
danger: { '& > div': { backgroundColor: theme.palette.error[500] } },
|
||||||
|
warning: { '& > div': { backgroundColor: theme.palette.error[500] } }
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const FormErrors = ({ errors, classes }) => {
|
||||||
|
const messageNode = (
|
||||||
|
<ul className={classes.list}>
|
||||||
|
{errors.map((error, index) => (
|
||||||
|
<li key={index}>
|
||||||
|
<Components.FormError error={error} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{!!errors.length && (
|
||||||
|
<Snackbar
|
||||||
|
open={true}
|
||||||
|
className={classNames('flash-message', classes.root , classes.danger)}
|
||||||
|
message={messageNode}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
replaceComponent('FormErrors', FormErrors, [withStyles, styles]);
|
|
@ -0,0 +1,95 @@
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
Components,
|
||||||
|
registerComponent,
|
||||||
|
instantiateComponent,
|
||||||
|
} from 'meteor/vulcan:core';
|
||||||
|
import withStyles from '@material-ui/core/styles/withStyles';
|
||||||
|
import Users from 'meteor/vulcan:users';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
|
||||||
|
const styles = theme => ({
|
||||||
|
root: {
|
||||||
|
minWidth: '320px'
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
class FormGroupNone extends PureComponent {
|
||||||
|
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
hidden,
|
||||||
|
classes,
|
||||||
|
currentUser,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (this.isAdmin && !Users.isAdmin(currentUser)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof hidden === 'function' ? hidden({ ...this.props }) : hidden) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
//do not display if no fields, no startComponent and no endComponent
|
||||||
|
if (!this.props.startComponent && !this.props.endComponent && !this.props.fields.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const anchorName = name.split('.').length > 1 ? name.split('.')[1] : name;
|
||||||
|
|
||||||
|
const FormComponents = this.props.formComponents;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
|
||||||
|
<a name={anchorName}/>
|
||||||
|
|
||||||
|
{instantiateComponent(this.props.startComponent)}
|
||||||
|
|
||||||
|
{this.props.fields.map(field => (
|
||||||
|
<FormComponents.FormComponent
|
||||||
|
key={field.name}
|
||||||
|
disabled={this.props.disabled}
|
||||||
|
{...field}
|
||||||
|
errors={this.props.errors}
|
||||||
|
throwError={this.props.throwError}
|
||||||
|
currentValues={this.props.currentValues}
|
||||||
|
updateCurrentValues={this.props.updateCurrentValues}
|
||||||
|
deletedValues={this.props.deletedValues}
|
||||||
|
addToDeletedValues={this.props.addToDeletedValues}
|
||||||
|
clearFieldErrors={this.props.clearFieldErrors}
|
||||||
|
formType={this.props.formType}
|
||||||
|
currentUser={this.props.currentUser}
|
||||||
|
formComponents={FormComponents}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{instantiateComponent(this.props.endComponent)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
FormGroupNone.propTypes = {
|
||||||
|
name: PropTypes.string,
|
||||||
|
order: PropTypes.number,
|
||||||
|
hidden: PropTypes.bool,
|
||||||
|
fields: PropTypes.array,
|
||||||
|
updateCurrentValues: PropTypes.func,
|
||||||
|
startComponent: PropTypes.node,
|
||||||
|
endComponent: PropTypes.node,
|
||||||
|
currentUser: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
registerComponent('FormGroupNone', FormGroupNone, [withStyles, styles]);
|
|
@ -0,0 +1,198 @@
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Components, registerComponent, instantiateComponent } from 'meteor/vulcan:core';
|
||||||
|
import Users from 'meteor/vulcan:users';
|
||||||
|
import withStyles from '@material-ui/core/styles/withStyles';
|
||||||
|
import Collapse from '@material-ui/core/Collapse';
|
||||||
|
import Divider from '@material-ui/core/Divider';
|
||||||
|
import Typography from '@material-ui/core/Typography';
|
||||||
|
import ExpandLessIcon from 'mdi-material-ui/ChevronUp';
|
||||||
|
import ExpandMoreIcon from 'mdi-material-ui/ChevronDown';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
|
||||||
|
const styles = theme => ({
|
||||||
|
root: {
|
||||||
|
minWidth: '320px',
|
||||||
|
},
|
||||||
|
divider: {
|
||||||
|
marginLeft: theme.spacing.unit * -3,
|
||||||
|
marginRight: theme.spacing.unit * -3,
|
||||||
|
},
|
||||||
|
subtitle1: {
|
||||||
|
marginTop: theme.spacing.unit * 5,
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
collapsible: {
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
typography: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
'& > div': {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
'& > div:first-child': {
|
||||||
|
...theme.typography.subtitle1,
|
||||||
|
},
|
||||||
|
paddingTop: theme.spacing.unit,
|
||||||
|
paddingBottom: theme.spacing.unit,
|
||||||
|
},
|
||||||
|
toggle: {
|
||||||
|
color: theme.palette.action.active,
|
||||||
|
},
|
||||||
|
entered: {
|
||||||
|
overflow: 'visible',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
class FormGroupWithLine extends PureComponent {
|
||||||
|
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.isAdmin = props.name === 'admin';
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
collapsed: props.startCollapsed || this.isAdmin,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
toggle = () => {
|
||||||
|
const collapsible = this.props.collapsible || this.isAdmin;
|
||||||
|
if (!collapsible) return;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
collapsed: !this.state.collapsed
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
renderHeading = () => {
|
||||||
|
const { classes } = this.props;
|
||||||
|
const collapsible = this.props.collapsible || this.isAdmin;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames(classes.subtitle1, collapsible && classes.collapsible)} onClick={this.toggle}>
|
||||||
|
|
||||||
|
<Divider className={classes.divider}/>
|
||||||
|
|
||||||
|
<Typography className={classes.typography} variant="subtitle1" gutterBottom>
|
||||||
|
<div>
|
||||||
|
{this.props.label}
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
collapsible &&
|
||||||
|
|
||||||
|
<div className={classes.toggle}>
|
||||||
|
{
|
||||||
|
this.state.collapsed
|
||||||
|
?
|
||||||
|
<ExpandMoreIcon/>
|
||||||
|
:
|
||||||
|
<ExpandLessIcon/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// if at least one of the fields in the group has an error, the group as a whole has an error
|
||||||
|
hasErrors = () => _.some(this.props.fields, field => {
|
||||||
|
return !!this.props.errors.filter(error => error.path === field.path).length;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
hidden,
|
||||||
|
classes,
|
||||||
|
currentUser,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (this.isAdmin && !Users.isAdmin(currentUser)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof hidden === 'function' ? hidden({ ...this.props }) : hidden) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
//do not display if no fields, no startComponent and no endComponent
|
||||||
|
if (!this.props.startComponent && !this.props.endComponent && !this.props.fields.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const anchorName = name.split('.').length > 1 ? name.split('.')[1] : name;
|
||||||
|
const collapseIn = !this.state.collapsed || this.hasErrors();
|
||||||
|
|
||||||
|
const FormComponents = this.props.formComponents;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
|
||||||
|
<a name={anchorName}/>
|
||||||
|
|
||||||
|
{
|
||||||
|
this.props.name === 'default'
|
||||||
|
?
|
||||||
|
null
|
||||||
|
:
|
||||||
|
this.renderHeading()
|
||||||
|
}
|
||||||
|
|
||||||
|
<Collapse classes={{ entered: classes.entered }} in={collapseIn}>
|
||||||
|
|
||||||
|
{instantiateComponent(this.props.startComponent)}
|
||||||
|
|
||||||
|
{this.props.fields.map(field => (
|
||||||
|
<FormComponents.FormComponent
|
||||||
|
key={field.name}
|
||||||
|
disabled={this.props.disabled}
|
||||||
|
{...field}
|
||||||
|
errors={this.props.errors}
|
||||||
|
throwError={this.props.throwError}
|
||||||
|
currentValues={this.props.currentValues}
|
||||||
|
updateCurrentValues={this.props.updateCurrentValues}
|
||||||
|
deletedValues={this.props.deletedValues}
|
||||||
|
addToDeletedValues={this.props.addToDeletedValues}
|
||||||
|
clearFieldErrors={this.props.clearFieldErrors}
|
||||||
|
formType={this.props.formType}
|
||||||
|
currentUser={this.props.currentUser}
|
||||||
|
formComponents={FormComponents}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{instantiateComponent(this.props.endComponent)}
|
||||||
|
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
FormGroupWithLine.propTypes = {
|
||||||
|
name: PropTypes.string,
|
||||||
|
label: PropTypes.string,
|
||||||
|
order: PropTypes.number,
|
||||||
|
fields: PropTypes.array,
|
||||||
|
collapsible: PropTypes.bool,
|
||||||
|
startCollapsed: PropTypes.bool,
|
||||||
|
updateCurrentValues: PropTypes.func,
|
||||||
|
startComponent: PropTypes.node,
|
||||||
|
endComponent: PropTypes.node,
|
||||||
|
currentUser: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
registerComponent('FormGroupWithLine', FormGroupWithLine, [withStyles, styles]);
|
|
@ -0,0 +1,13 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { replaceComponent } from 'meteor/vulcan:core';
|
||||||
|
import Delete from 'mdi-material-ui/Delete';
|
||||||
|
import Plus from 'mdi-material-ui/Plus';
|
||||||
|
|
||||||
|
|
||||||
|
const IconRemove = () => <Delete/>;
|
||||||
|
|
||||||
|
replaceComponent('IconRemove', IconRemove);
|
||||||
|
|
||||||
|
const IconAdd = () => <Plus/>;
|
||||||
|
|
||||||
|
replaceComponent('IconAdd', IconAdd);
|
|
@ -0,0 +1,24 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { replaceComponent } from 'meteor/vulcan:core';
|
||||||
|
|
||||||
|
import Grid from '@material-ui/core/Grid';
|
||||||
|
import Typography from '@material-ui/core/Typography';
|
||||||
|
|
||||||
|
const FormNestedArrayLayout = ({ hasErrors, label, content }) => (
|
||||||
|
<div>
|
||||||
|
<Typography component="label" variant="caption" style={{ fontSize: 16 }}>
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
<div>{content}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
FormNestedArrayLayout.propTypes = {
|
||||||
|
hasErrors: PropTypes.bool,
|
||||||
|
label: PropTypes.node,
|
||||||
|
content: PropTypes.node,
|
||||||
|
};
|
||||||
|
replaceComponent({
|
||||||
|
name: 'FormNestedArrayLayout',
|
||||||
|
component: FormNestedArrayLayout,
|
||||||
|
});
|
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { replaceComponent } from 'meteor/vulcan:core';
|
||||||
|
// import { FormattedMessage } from 'meteor/vulcan:i18n';
|
||||||
|
import withStyles from '@material-ui/core/styles/withStyles';
|
||||||
|
import Divider from '@material-ui/core/Divider';
|
||||||
|
|
||||||
|
const styles = theme => ({
|
||||||
|
divider: {
|
||||||
|
// marginLeft: -24,
|
||||||
|
// marginRight: -24,
|
||||||
|
marginTop: 16,
|
||||||
|
marginBottom: 23,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const FormNestedDivider = ({ classes, label, addItem }) => <Divider className={classes.divider} />;
|
||||||
|
|
||||||
|
FormNestedDivider.propTypes = {
|
||||||
|
classes: PropTypes.object.isRequired,
|
||||||
|
label: PropTypes.string,
|
||||||
|
addItem: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
replaceComponent('FormNestedDivider', FormNestedDivider, [withStyles, styles]);
|
|
@ -0,0 +1,20 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Components, registerComponent } from 'meteor/vulcan:core';
|
||||||
|
import { FormattedMessage } from 'meteor/vulcan:i18n';
|
||||||
|
import Grid from '@material-ui/core/Grid';
|
||||||
|
|
||||||
|
const FormNestedFoot = ({ label, addItem }) => (
|
||||||
|
<Grid container spacing={0} justify="flex-end">
|
||||||
|
<Components.Button color="primary" iconButton onClick={addItem}>
|
||||||
|
<Components.IconAdd/>
|
||||||
|
</Components.Button>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
|
||||||
|
FormNestedFoot.propTypes = {
|
||||||
|
label: PropTypes.string,
|
||||||
|
addItem: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
registerComponent('FormNestedFoot', FormNestedFoot);
|
|
@ -0,0 +1,13 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { replaceComponent } from 'meteor/vulcan:core';
|
||||||
|
import { FormattedMessage } from 'meteor/vulcan:i18n';
|
||||||
|
|
||||||
|
const FormNestedHead = ({ label, addItem }) => <span/>;
|
||||||
|
|
||||||
|
FormNestedHead.propTypes = {
|
||||||
|
label: PropTypes.string,
|
||||||
|
addItem: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
replaceComponent('FormNestedHead', FormNestedHead);
|
136
packages/vulcan-ui-material/lib/components/forms/FormSubmit.jsx
Normal file
136
packages/vulcan-ui-material/lib/components/forms/FormSubmit.jsx
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Components, replaceComponent } from 'meteor/vulcan:core';
|
||||||
|
import { intlShape } from 'meteor/vulcan:i18n';
|
||||||
|
import withStyles from '@material-ui/core/styles/withStyles';
|
||||||
|
import Button from '@material-ui/core/Button';
|
||||||
|
import IconButton from '@material-ui/core/IconButton';
|
||||||
|
import DeleteIcon from 'mdi-material-ui/Delete';
|
||||||
|
import Tooltip from '@material-ui/core/Tooltip';
|
||||||
|
import { FormattedMessage } from 'meteor/vulcan:i18n';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
|
||||||
|
const styles = theme => ({
|
||||||
|
root: {
|
||||||
|
textAlign: 'center',
|
||||||
|
marginTop: theme.spacing.unit * 4,
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
margin: theme.spacing.unit,
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
float: 'left',
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
margin: 3,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const FormSubmit = ({
|
||||||
|
submitLabel,
|
||||||
|
cancelLabel,
|
||||||
|
cancelCallback,
|
||||||
|
revertLabel,
|
||||||
|
revertCallback,
|
||||||
|
document,
|
||||||
|
deleteDocument,
|
||||||
|
collectionName,
|
||||||
|
classes
|
||||||
|
}, {
|
||||||
|
intl,
|
||||||
|
isChanged,
|
||||||
|
clearForm,
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
if (typeof isChanged !== 'function') {
|
||||||
|
isChanged = () => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
|
||||||
|
{
|
||||||
|
deleteDocument
|
||||||
|
?
|
||||||
|
<Tooltip id={`tooltip-delete-${collectionName}`}
|
||||||
|
classes={{ tooltip: classNames('delete-button', classes.tooltip) }}
|
||||||
|
title={intl.formatMessage({ id: 'forms.delete' })}
|
||||||
|
placement="bottom">
|
||||||
|
<IconButton onClick={deleteDocument} className={classes.delete}>
|
||||||
|
<DeleteIcon/>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
:
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
cancelCallback
|
||||||
|
?
|
||||||
|
<Button variant="contained"
|
||||||
|
className={classNames('cancel-button', classes.button)}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
cancelCallback(document);
|
||||||
|
}}>
|
||||||
|
{cancelLabel ? cancelLabel : <FormattedMessage id="forms.cancel"/>}
|
||||||
|
</Button>
|
||||||
|
:
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
revertCallback
|
||||||
|
?
|
||||||
|
<Button variant="contained"
|
||||||
|
className={classNames('revert-button', classes.button)}
|
||||||
|
disabled={!isChanged()}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
clearForm({ clearErrors: true, clearCurrentValues: true, clearDeletedValues: true });
|
||||||
|
revertCallback(document);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{revertLabel ? revertLabel : <FormattedMessage id="forms.revert"/>}
|
||||||
|
</Button>
|
||||||
|
:
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
<Button variant="contained"
|
||||||
|
type="submit"
|
||||||
|
color="secondary"
|
||||||
|
className={classNames('submit-button', classes.button)}
|
||||||
|
disabled={!isChanged()}
|
||||||
|
>
|
||||||
|
{submitLabel ? submitLabel : <FormattedMessage id="forms.submit"/>}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
FormSubmit.propTypes = {
|
||||||
|
submitLabel: PropTypes.node,
|
||||||
|
cancelLabel: PropTypes.node,
|
||||||
|
revertLabel: PropTypes.node,
|
||||||
|
cancelCallback: PropTypes.func,
|
||||||
|
revertCallback: PropTypes.func,
|
||||||
|
document: PropTypes.object,
|
||||||
|
deleteDocument: PropTypes.func,
|
||||||
|
collectionName: PropTypes.string,
|
||||||
|
classes: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
FormSubmit.contextTypes = {
|
||||||
|
intl: intlShape,
|
||||||
|
isChanged: PropTypes.func,
|
||||||
|
clearForm: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
replaceComponent('FormSubmit', FormSubmit, [withStyles, styles]);
|
|
@ -0,0 +1,84 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { instantiateComponent } from 'meteor/vulcan:core';
|
||||||
|
import withStyles from '@material-ui/core/styles/withStyles';
|
||||||
|
import InputAdornment from '@material-ui/core/InputAdornment';
|
||||||
|
import IconButton from '@material-ui/core/IconButton';
|
||||||
|
import CloseIcon from 'mdi-material-ui/CloseCircle';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
|
||||||
|
export const styles = theme => ({
|
||||||
|
inputAdornment: {
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
marginTop: '0 !important',
|
||||||
|
'& > *': {
|
||||||
|
verticalAlign: 'bottom',
|
||||||
|
},
|
||||||
|
'& > svg': {
|
||||||
|
color: theme.palette.common.darkBlack,
|
||||||
|
},
|
||||||
|
'& > * + *': {
|
||||||
|
marginLeft: 8,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clearButton: {
|
||||||
|
opacity: 0,
|
||||||
|
'& svg': {
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
},
|
||||||
|
marginRight: -12,
|
||||||
|
marginLeft: -4,
|
||||||
|
'&:first-child': {
|
||||||
|
marginLeft: -12,
|
||||||
|
},
|
||||||
|
transition: theme.transitions.create('opacity', {
|
||||||
|
duration: theme.transitions.duration.short,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
urlButton: {
|
||||||
|
verticalAlign: 'bottom',
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
fontSize: 20,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const EndAdornment = (props) => {
|
||||||
|
const { classes, value, addonAfter, changeValue, hideClear, disabled } = props;
|
||||||
|
|
||||||
|
if (!addonAfter && (!changeValue || hideClear || disabled)) return null;
|
||||||
|
const hasValue = !!value || value === 0;
|
||||||
|
|
||||||
|
const clearButton = changeValue && !hideClear && !disabled &&
|
||||||
|
<IconButton className={classNames('clear-button', classes.clearButton, hasValue && 'clear-enabled')}
|
||||||
|
onClick={event => {
|
||||||
|
event.preventDefault();
|
||||||
|
changeValue(null);
|
||||||
|
}}
|
||||||
|
tabIndex="-1"
|
||||||
|
>
|
||||||
|
<CloseIcon/>
|
||||||
|
</IconButton>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputAdornment classes={{ root: classes.inputAdornment }} position="end">
|
||||||
|
{instantiateComponent(addonAfter)}
|
||||||
|
{clearButton}
|
||||||
|
</InputAdornment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
EndAdornment.propTypes = {
|
||||||
|
classes: PropTypes.object.isRequired,
|
||||||
|
value: PropTypes.any,
|
||||||
|
changeValue: PropTypes.func,
|
||||||
|
hideClear: PropTypes.bool,
|
||||||
|
addonAfter: PropTypes.oneOfType([PropTypes.string, PropTypes.node, PropTypes.func]),
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default withStyles(styles)(EndAdornment);
|
|
@ -0,0 +1,150 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import createReactClass from 'create-react-class';
|
||||||
|
import ComponentMixin from './mixins/component';
|
||||||
|
import withStyles from '@material-ui/core/styles/withStyles';
|
||||||
|
import FormGroup from '@material-ui/core/FormGroup';
|
||||||
|
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
||||||
|
import MuiFormControl from './MuiFormControl';
|
||||||
|
import MuiFormHelper from './MuiFormHelper';
|
||||||
|
import Checkbox from '@material-ui/core/Checkbox';
|
||||||
|
import Switch from '@material-ui/core/Switch';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
|
||||||
|
const styles = theme => ({
|
||||||
|
group: {
|
||||||
|
marginTop: '8px',
|
||||||
|
},
|
||||||
|
twoColumn: {
|
||||||
|
display: 'block',
|
||||||
|
[theme.breakpoints.down('md')]: {
|
||||||
|
'& > label': {
|
||||||
|
marginRight: theme.spacing.unit * 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
|
'& > label': {
|
||||||
|
width: '49%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
threeColumn: {
|
||||||
|
display: 'block',
|
||||||
|
[theme.breakpoints.down('xs')]: {
|
||||||
|
'& > label': {
|
||||||
|
marginRight: theme.spacing.unit * 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[theme.breakpoints.up('xs')]: {
|
||||||
|
'& > label': {
|
||||||
|
width: '49%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
|
'& > label': {
|
||||||
|
width: '32%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const MuiCheckboxGroup = createReactClass({
|
||||||
|
|
||||||
|
mixins: [ComponentMixin],
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
options: PropTypes.array.isRequired,
|
||||||
|
classes: PropTypes.object.isRequired,
|
||||||
|
variant: PropTypes.oneOf(['checkbox', 'switch']),
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount: function () {
|
||||||
|
if (this.props.refFunction) {
|
||||||
|
this.props.refFunction(this);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function () {
|
||||||
|
return {
|
||||||
|
label: '',
|
||||||
|
help: null,
|
||||||
|
variant: 'checkbox',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
changeCheckbox: function () {
|
||||||
|
const value = [];
|
||||||
|
this.props.options.forEach(function (option, key) {
|
||||||
|
if (this[this.props.name + '-' + option.value].checked) {
|
||||||
|
value.push(option.value);
|
||||||
|
}
|
||||||
|
}.bind(this));
|
||||||
|
|
||||||
|
this.props.onChange(value);
|
||||||
|
},
|
||||||
|
|
||||||
|
validate: function () {
|
||||||
|
if (this.props.onBlur) {
|
||||||
|
this.props.onBlur();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderElement: function () {
|
||||||
|
const controls = this.props.options.map((checkbox, key) => {
|
||||||
|
let value = checkbox.value;
|
||||||
|
let checked = (this.props.value.indexOf(value) !== -1);
|
||||||
|
let disabled = checkbox.disabled || this.props.disabled;
|
||||||
|
const Component = this.props.variant === 'switch' ? Switch : Checkbox;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControlLabel
|
||||||
|
key={key}
|
||||||
|
control={
|
||||||
|
<Component
|
||||||
|
inputRef={(c) => this[this.props.name + '-' + value] = c}
|
||||||
|
checked={checked}
|
||||||
|
onChange={this.changeCheckbox}
|
||||||
|
value={value}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={checkbox.label}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const maxLength = this.props.options.reduce((max, option) =>
|
||||||
|
option.label.length > max ? option.label.length : max, 0);
|
||||||
|
|
||||||
|
const columnClass = maxLength < 20 ? 'threeColumn' : maxLength < 30 ? 'twoColumn' : '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup className={classNames(this.props.classes.group, this.props.classes[columnClass])}>
|
||||||
|
{controls}
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function () {
|
||||||
|
|
||||||
|
if (this.props.layout === 'elementOnly') {
|
||||||
|
return (
|
||||||
|
<div>{this.renderElement()}</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MuiFormControl{...this.getFormControlProperties()} fakeLabel={true}>
|
||||||
|
{this.renderElement()}
|
||||||
|
<MuiFormHelper {...this.getFormHelperProperties()}/>
|
||||||
|
</MuiFormControl>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export default withStyles(styles)(MuiCheckboxGroup);
|
|
@ -0,0 +1,91 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import createReactClass from 'create-react-class';
|
||||||
|
import InputLabel from '@material-ui/core/InputLabel';
|
||||||
|
import FormControl from '@material-ui/core/FormControl';
|
||||||
|
import FormLabel from '@material-ui/core/FormLabel';
|
||||||
|
|
||||||
|
|
||||||
|
//noinspection JSUnusedGlobalSymbols
|
||||||
|
const MuiFormControl = createReactClass({
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
label: PropTypes.node,
|
||||||
|
children: PropTypes.node,
|
||||||
|
required: PropTypes.bool,
|
||||||
|
hasErrors: PropTypes.bool,
|
||||||
|
fakeLabel: PropTypes.bool,
|
||||||
|
hideLabel: PropTypes.bool,
|
||||||
|
layout: PropTypes.oneOf(['horizontal', 'vertical', 'elementOnly']),
|
||||||
|
htmlFor: PropTypes.string
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function () {
|
||||||
|
return {
|
||||||
|
label: '',
|
||||||
|
required: false,
|
||||||
|
hasErrors: false,
|
||||||
|
fakeLabel: false,
|
||||||
|
hideLabel: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
renderRequiredSymbol: function () {
|
||||||
|
if (this.props.required === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span className="required-symbol"> *</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderLabel: function () {
|
||||||
|
if (this.props.layout === 'elementOnly' || this.props.hideLabel) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.fakeLabel) {
|
||||||
|
return (
|
||||||
|
<FormLabel className="control-label legend"
|
||||||
|
component="legend"
|
||||||
|
data-required={this.props.required}
|
||||||
|
>
|
||||||
|
{this.props.label}
|
||||||
|
{this.renderRequiredSymbol()}
|
||||||
|
</FormLabel>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const shrink = ['date', 'time', 'datetime'].includes(this.props.inputType) ? true : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputLabel className="control-label"
|
||||||
|
data-required={this.props.required}
|
||||||
|
htmlFor={this.props.htmlFor}
|
||||||
|
shrink={shrink}
|
||||||
|
>
|
||||||
|
{this.props.label}
|
||||||
|
{this.renderRequiredSymbol()}
|
||||||
|
</InputLabel>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function () {
|
||||||
|
const { layout, className, children, hasErrors } = this.props;
|
||||||
|
|
||||||
|
if (layout === 'elementOnly') {
|
||||||
|
return <span>{children}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl component="fieldset" error={hasErrors} fullWidth={true} className={className}>
|
||||||
|
{this.renderLabel()}
|
||||||
|
{children}
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export default MuiFormControl;
|
|
@ -0,0 +1,75 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { instantiateComponent, Components } from 'meteor/vulcan:core';
|
||||||
|
import withStyles from '@material-ui/core/styles/withStyles';
|
||||||
|
import FormHelperText from '@material-ui/core/FormHelperText';
|
||||||
|
import { FormattedMessage } from 'meteor/vulcan:i18n';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
|
||||||
|
export const styles = theme => ({
|
||||||
|
|
||||||
|
error: {
|
||||||
|
color: theme.palette.error.main,
|
||||||
|
},
|
||||||
|
|
||||||
|
formHelperText: {
|
||||||
|
display: 'flex',
|
||||||
|
'& :first-child': {
|
||||||
|
flexGrow: 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const MuiFormHelper = (props) => {
|
||||||
|
const {
|
||||||
|
classes,
|
||||||
|
help,
|
||||||
|
errors,
|
||||||
|
hasErrors,
|
||||||
|
showCharsRemaining,
|
||||||
|
charsRemaining,
|
||||||
|
charsCount,
|
||||||
|
max,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
if (!help && !hasErrors && !showCharsRemaining) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorMessage = hasErrors &&
|
||||||
|
<Components.FormError error={errors[0]} />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormHelperText className={classes.formHelperText} error={hasErrors}>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{
|
||||||
|
hasErrors ? errorMessage : help
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{
|
||||||
|
showCharsRemaining &&
|
||||||
|
|
||||||
|
<span className={charsRemaining < 0 ? classes.error : null}>
|
||||||
|
{charsCount} / {max}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
</FormHelperText>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
MuiFormHelper.propTypes = {
|
||||||
|
classes: PropTypes.object.isRequired,
|
||||||
|
value: PropTypes.any,
|
||||||
|
changeValue: PropTypes.func,
|
||||||
|
addonAfter: PropTypes.oneOfType([PropTypes.string, PropTypes.node, PropTypes.func]),
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default withStyles(styles)(MuiFormHelper);
|
|
@ -0,0 +1,138 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import createReactClass from 'create-react-class';
|
||||||
|
import withStyles from '@material-ui/core/styles/withStyles';
|
||||||
|
import ComponentMixin from './mixins/component';
|
||||||
|
import MuiFormControl from './MuiFormControl';
|
||||||
|
import MuiFormHelper from './MuiFormHelper';
|
||||||
|
import Input from '@material-ui/core/Input';
|
||||||
|
import StartAdornment, { hideStartAdornment, fixUrl } from './StartAdornment';
|
||||||
|
import EndAdornment from './EndAdornment';
|
||||||
|
|
||||||
|
|
||||||
|
export const styles = theme => ({
|
||||||
|
inputRoot: {
|
||||||
|
'& .clear-enabled': { opacity: 0 },
|
||||||
|
'&:hover .clear-enabled': { opacity: 0.54 },
|
||||||
|
},
|
||||||
|
inputFocused: {
|
||||||
|
'& .clear-enabled': { opacity: 0.54 }
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
//noinspection JSUnusedGlobalSymbols
|
||||||
|
const MuiInput = createReactClass({
|
||||||
|
element: null,
|
||||||
|
|
||||||
|
mixins: [ComponentMixin],
|
||||||
|
|
||||||
|
displayName: 'MuiInput',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
type: PropTypes.oneOf([
|
||||||
|
'color',
|
||||||
|
'date',
|
||||||
|
'datetime',
|
||||||
|
'datetime-local',
|
||||||
|
'email',
|
||||||
|
'hidden',
|
||||||
|
'month',
|
||||||
|
'number',
|
||||||
|
'password',
|
||||||
|
'range',
|
||||||
|
'search',
|
||||||
|
'tel',
|
||||||
|
'text',
|
||||||
|
'time',
|
||||||
|
'url',
|
||||||
|
'week'
|
||||||
|
]),
|
||||||
|
errors: PropTypes.array,
|
||||||
|
placeholder: PropTypes.string,
|
||||||
|
formatValue: PropTypes.func,
|
||||||
|
scrubValue: PropTypes.func,
|
||||||
|
hideClear: PropTypes.bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function () {
|
||||||
|
return {
|
||||||
|
type: 'text',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
handleChange: function (event) {
|
||||||
|
let value = event.target.value;
|
||||||
|
if (this.props.scrubValue) {
|
||||||
|
value = this.props.scrubValue(value);
|
||||||
|
}
|
||||||
|
this.changeValue(value);
|
||||||
|
},
|
||||||
|
|
||||||
|
changeValue: function (value) {
|
||||||
|
this.props.onChange(value);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleBlur: function (event) {
|
||||||
|
const { type, value } = this.props;
|
||||||
|
|
||||||
|
if (type === 'url' && !!value && value !== fixUrl(value)) {
|
||||||
|
this.changeValue(fixUrl(value));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function () {
|
||||||
|
const startAdornment = hideStartAdornment(this.props) ? null :
|
||||||
|
<StartAdornment {...this.props}
|
||||||
|
classes={null}
|
||||||
|
changeValue={this.changeValue}
|
||||||
|
/>;
|
||||||
|
const endAdornment =
|
||||||
|
<EndAdornment {...this.props}
|
||||||
|
classes={null}
|
||||||
|
changeValue={this.changeValue}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
let element = this.renderElement(startAdornment, endAdornment);
|
||||||
|
|
||||||
|
if (this.props.layout === 'elementOnly' || this.props.type === 'hidden') {
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MuiFormControl {...this.getFormControlProperties()} htmlFor={this.getId()}>
|
||||||
|
{element}
|
||||||
|
<MuiFormHelper {...this.getFormHelperProperties()}/>
|
||||||
|
</MuiFormControl>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderElement: function (startAdornment, endAdornment) {
|
||||||
|
const { classes, disabled, autoFocus, formatValue } = this.props;
|
||||||
|
const value = formatValue ? formatValue(this.props.value) : this.props.value;
|
||||||
|
const options = this.props.options || {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
ref={c => (this.element = c)}
|
||||||
|
{...this.cleanProps(this.props)}
|
||||||
|
id={this.getId()}
|
||||||
|
value={value}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
onBlur={this.handleBlur}
|
||||||
|
disabled={disabled}
|
||||||
|
rows={options.rows || this.props.rows}
|
||||||
|
autoFocus={options.autoFocus || autoFocus}
|
||||||
|
startAdornment={startAdornment}
|
||||||
|
endAdornment={endAdornment}
|
||||||
|
placeholder={this.props.placeholder}
|
||||||
|
classes={{ root: classes.inputRoot, focused: classes.inputFocused }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export default withStyles(styles)(MuiInput);
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue