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",
|
||||
"avoid-escape"
|
||||
],
|
||||
"react/display-name": 1,
|
||||
"react/prop-types": 0,
|
||||
"semi": [1, "always"]
|
||||
},
|
||||
|
|
272
package-lock.json
generated
272
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "Vulcan",
|
||||
"version": "1.12.16",
|
||||
"version": "1.12.17",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@ -330,13 +330,22 @@
|
|||
"warning": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz",
|
||||
"integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==",
|
||||
"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": {
|
||||
"loose-envify": "^1.3.1",
|
||||
"object-assign": "^4.1.1"
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
|
||||
"integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"kind-of": "^3.0.2",
|
||||
"longest": "^1.0.1",
|
||||
|
@ -898,11 +906,12 @@
|
|||
}
|
||||
},
|
||||
"apollo-errors": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/apollo-errors/-/apollo-errors-1.5.1.tgz",
|
||||
"integrity": "sha512-gYAceMzNJfF+mUHH2/4UcZTkZtDY54arCTKGbKa7WU5IXnTJ4V+P94wHodcDkLLHWpHL8SW1hEgjN5ZINcPb1w==",
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/apollo-errors/-/apollo-errors-1.9.0.tgz",
|
||||
"integrity": "sha512-XVukHd0KLvgY6tNjsPS3/Re3U6RQlTKrTbIpqqeTMo2N34uQMr+H1UheV21o8hOZBAFosvBORVricJiP5vfmrw==",
|
||||
"requires": {
|
||||
"es6-error": "^4.0.0"
|
||||
"assert": "^1.4.1",
|
||||
"extendable-error": "^0.1.5"
|
||||
}
|
||||
},
|
||||
"apollo-link": {
|
||||
|
@ -1289,6 +1298,14 @@
|
|||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/assert-err/-/assert-err-1.1.0.tgz",
|
||||
|
@ -2384,7 +2401,7 @@
|
|||
},
|
||||
"cheerio": {
|
||||
"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=",
|
||||
"requires": {
|
||||
"css-select": "~1.2.0",
|
||||
|
@ -2831,7 +2848,7 @@
|
|||
},
|
||||
"css-select": {
|
||||
"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=",
|
||||
"requires": {
|
||||
"boolbase": "~1.0.0",
|
||||
|
@ -3370,11 +3387,6 @@
|
|||
"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": {
|
||||
"version": "2.0.3",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz",
|
||||
|
@ -6168,8 +6185,7 @@
|
|||
"longest": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
|
||||
"integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=",
|
||||
"optional": true
|
||||
"integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc="
|
||||
},
|
||||
"loose-envify": {
|
||||
"version": "1.3.1",
|
||||
|
@ -6261,7 +6277,7 @@
|
|||
"process": "^0.11.9",
|
||||
"punycode": "^1.4.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",
|
||||
"string_decoder": "^1.0.1",
|
||||
"timers-browserify": "^1.4.2",
|
||||
|
@ -6711,12 +6727,19 @@
|
|||
"version": "git+https://github.com/meteor/readable-stream.git#c688cdd193549919b840e8d72a86682d91961e12",
|
||||
"from": "git+https://github.com/meteor/readable-stream.git",
|
||||
"requires": {
|
||||
"inherits": "~2.0.3",
|
||||
"inherits": "~2.0.1",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.0",
|
||||
"process-nextick-args": "~1.0.6",
|
||||
"safe-buffer": "^5.0.1",
|
||||
"string_decoder": "~1.0.0",
|
||||
"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": {
|
||||
|
@ -7790,31 +7813,31 @@
|
|||
}
|
||||
},
|
||||
"react-bootstrap": {
|
||||
"version": "1.0.0-beta.3",
|
||||
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.0.0-beta.3.tgz",
|
||||
"integrity": "sha512-/eiSmRZE92q6m7uen3oAsOTGY4uBJkZDv32fwxUeyjesf834GUDaEhu1dzj10fmxSeVHW9O6UOKj9GkbwIIkMg==",
|
||||
"version": "1.0.0-beta.5",
|
||||
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.0.0-beta.5.tgz",
|
||||
"integrity": "sha512-Osm0OtTbYwfsT1rpu88ESWuAHZxfaHFNKFiW8w3w+6YY9/bLEPHbGRZA6W21fg5yvcuKN9hJKT857TTHgY7SoQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"@babel/runtime": "^7.2.0",
|
||||
"@react-bootstrap/react-popper": "1.2.1",
|
||||
"classnames": "^2.2.6",
|
||||
"dom-helpers": "^3.2.0",
|
||||
"dom-helpers": "^3.4.0",
|
||||
"invariant": "^2.2.3",
|
||||
"keycode": "^2.1.2",
|
||||
"popper.js": "^1.14.3",
|
||||
"popper.js": "^1.14.6",
|
||||
"prop-types": "^15.6.2",
|
||||
"prop-types-extra": "^1.1.0",
|
||||
"react-context-toolbox": "^1.2.3",
|
||||
"react-overlays": "^1.0.0-beta.17",
|
||||
"react-context-toolbox": "^2.0.2",
|
||||
"react-overlays": "^1.0.0",
|
||||
"react-prop-types": "^0.4.0",
|
||||
"react-transition-group": "^2.4.0",
|
||||
"react-transition-group": "^2.5.1",
|
||||
"uncontrollable": "^6.0.0",
|
||||
"warning": "^4.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.2.0.tgz",
|
||||
"integrity": "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg==",
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.4.tgz",
|
||||
"integrity": "sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g==",
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.12.0"
|
||||
}
|
||||
|
@ -7824,6 +7847,14 @@
|
|||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
|
||||
"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": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||
|
@ -7833,12 +7864,23 @@
|
|||
}
|
||||
},
|
||||
"prop-types": {
|
||||
"version": "15.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz",
|
||||
"integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==",
|
||||
"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.3.1",
|
||||
"object-assign": "^4.1.1"
|
||||
"loose-envify": "^1.4.0",
|
||||
"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": {
|
||||
|
@ -7847,9 +7889,9 @@
|
|||
"integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg=="
|
||||
},
|
||||
"warning": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.2.tgz",
|
||||
"integrity": "sha512-wbTp09q/9C+jJn4KKJfJfoS6VleK/Dti0yqWSm6KMvJ4MRCXFQNapHuJXutJIrWV0Cf4AhTdeIe4qdKHR1+Hug==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
|
||||
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
|
@ -7882,9 +7924,9 @@
|
|||
}
|
||||
},
|
||||
"react-context-toolbox": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-context-toolbox/-/react-context-toolbox-1.2.3.tgz",
|
||||
"integrity": "sha512-ArHw0UFDM6X8Z9lHZ1rZOhMcn8TXWC9y9sFpeJm11YTIlQsN4A0MadKcps2pCGMwWKmH0o/67t+TmVapwWm5Sw=="
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-context-toolbox/-/react-context-toolbox-2.0.2.tgz",
|
||||
"integrity": "sha512-tY4j0imkYC3n5ZlYSgFkaw7fmlCp3IoQQ6DxpqeNHzcD0hf+6V+/HeJxviLUZ1Rv1Yn3N3xyO2EhkkZwHn0m1A=="
|
||||
},
|
||||
"react-cookie": {
|
||||
"version": "2.1.6",
|
||||
|
@ -8001,9 +8043,9 @@
|
|||
}
|
||||
},
|
||||
"react-is": {
|
||||
"version": "16.4.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.4.1.tgz",
|
||||
"integrity": "sha512-xpb0PpALlFWNw/q13A+1aHeyJyLYCg0/cCHPUA43zYluZuIPHaHL3k8OBsTgQtxqW0FhyDEMvi8fZ/+7+r4OSQ=="
|
||||
"version": "16.8.4",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.4.tgz",
|
||||
"integrity": "sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA=="
|
||||
},
|
||||
"react-lifecycles-compat": {
|
||||
"version": "3.0.4",
|
||||
|
@ -8038,9 +8080,9 @@
|
|||
"integrity": "sha512-IBivBP7xayM7SbbVlAnKgHgoWdfCVqnNBNgQRY5x9iFQm55tFdolR02hX1fCJJtTEKnbaL1stB72/TZc6+p2+Q=="
|
||||
},
|
||||
"react-overlays": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-1.0.0.tgz",
|
||||
"integrity": "sha512-YDuUwqWBuVvQvvxPTxKpCuaEZRegDhfgJdQUbVmWVI4gag53zukN/6tNxt0XZpgODQVzLf/w7dFuoDq7YMhygg==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-1.2.0.tgz",
|
||||
"integrity": "sha512-i/FCV8wR6aRaI+Kz/dpJhOdyx+ah2tN1RhT9InPrexyC4uzf3N4bNayFTGtUeQVacj57j1Mqh1CwV60/5153Iw==",
|
||||
"requires": {
|
||||
"classnames": "^2.2.6",
|
||||
"dom-helpers": "^3.4.0",
|
||||
|
@ -8048,13 +8090,14 @@
|
|||
"prop-types-extra": "^1.1.0",
|
||||
"react-context-toolbox": "^2.0.2",
|
||||
"react-popper": "^1.3.2",
|
||||
"uncontrollable": "^6.0.0",
|
||||
"warning": "^4.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.2.0.tgz",
|
||||
"integrity": "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg==",
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.4.tgz",
|
||||
"integrity": "sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g==",
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.12.0"
|
||||
}
|
||||
|
@ -8072,19 +8115,23 @@
|
|||
"@babel/runtime": "^7.1.2"
|
||||
}
|
||||
},
|
||||
"prop-types": {
|
||||
"version": "15.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz",
|
||||
"integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==",
|
||||
"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": {
|
||||
"loose-envify": "^1.3.1",
|
||||
"object-assign": "^4.1.1"
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"react-context-toolbox": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-context-toolbox/-/react-context-toolbox-2.0.2.tgz",
|
||||
"integrity": "sha512-tY4j0imkYC3n5ZlYSgFkaw7fmlCp3IoQQ6DxpqeNHzcD0hf+6V+/HeJxviLUZ1Rv1Yn3N3xyO2EhkkZwHn0m1A=="
|
||||
"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": {
|
||||
"version": "0.12.1",
|
||||
|
@ -8092,9 +8139,9 @@
|
|||
"integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg=="
|
||||
},
|
||||
"warning": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.2.tgz",
|
||||
"integrity": "sha512-wbTp09q/9C+jJn4KKJfJfoS6VleK/Dti0yqWSm6KMvJ4MRCXFQNapHuJXutJIrWV0Cf4AhTdeIe4qdKHR1+Hug==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
|
||||
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
|
@ -8111,9 +8158,9 @@
|
|||
}
|
||||
},
|
||||
"react-popper": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.2.tgz",
|
||||
"integrity": "sha512-UbFWj55Yt9uqvy0oZ+vULDL2Bw1oxeZF9/JzGyxQ5ypgauRH/XlarA5+HLZWro/Zss6Ht2kqpegtb6sYL8GUGw==",
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.3.tgz",
|
||||
"integrity": "sha512-ynMZBPkXONPc5K4P5yFWgZx5JGAUIP3pGGLNs58cfAPgK67olx7fmLp+AdpZ0+GoQ+ieFDa/z4cdV6u7sioH6w==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.1.2",
|
||||
"create-react-context": "<=0.2.2",
|
||||
|
@ -8124,9 +8171,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.2.0.tgz",
|
||||
"integrity": "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg==",
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.4.tgz",
|
||||
"integrity": "sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g==",
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.12.0"
|
||||
}
|
||||
|
@ -8140,13 +8187,22 @@
|
|||
"gud": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"prop-types": {
|
||||
"version": "15.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz",
|
||||
"integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==",
|
||||
"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": {
|
||||
"loose-envify": "^1.3.1",
|
||||
"object-assign": "^4.1.1"
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
|
@ -8160,9 +8216,9 @@
|
|||
"integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q=="
|
||||
},
|
||||
"warning": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.2.tgz",
|
||||
"integrity": "sha512-wbTp09q/9C+jJn4KKJfJfoS6VleK/Dti0yqWSm6KMvJ4MRCXFQNapHuJXutJIrWV0Cf4AhTdeIe4qdKHR1+Hug==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
|
||||
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
|
@ -8202,27 +8258,6 @@
|
|||
"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": {
|
||||
"version": "0.24.4",
|
||||
"resolved": "https://registry.npmjs.org/react-router-bootstrap/-/react-router-bootstrap-0.24.4.tgz",
|
||||
|
@ -9569,9 +9604,9 @@
|
|||
"optional": true
|
||||
},
|
||||
"uncontrollable": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-6.0.0.tgz",
|
||||
"integrity": "sha512-gmy2ESW40LGbijSbW5piBGiPv55IgyDbjQcMr7LkDR5icpw/06UgMqULAGDBAcFn2a9d/SRPgcb3oo8hdEUfIw==",
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-6.1.0.tgz",
|
||||
"integrity": "sha512-2TzEm0pLKauMBZfAZXsgQvLpZHEp95891frCZdGDrSG7dWYaIQhedwLAzi0X8pR8KHNqlmuYEb2cEgbQzr050A==",
|
||||
"requires": {
|
||||
"invariant": "^2.2.4"
|
||||
},
|
||||
|
@ -9717,6 +9752,21 @@
|
|||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
@ -9852,7 +9902,7 @@
|
|||
},
|
||||
"chalk": {
|
||||
"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=",
|
||||
"requires": {
|
||||
"ansi-styles": "^2.2.1",
|
||||
|
|
11
package.json
11
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "Vulcan",
|
||||
"version": "1.12.17",
|
||||
"version": "1.13.0",
|
||||
"engines": {
|
||||
"npm": "^3.0"
|
||||
},
|
||||
|
@ -21,7 +21,7 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.0.0-beta.55",
|
||||
"@babel/runtime": "7.1.2",
|
||||
"analytics-node": "^2.1.1",
|
||||
"apollo-cache-inmemory": "^1.4.2",
|
||||
"apollo-client": "2.4.12",
|
||||
|
@ -30,6 +30,7 @@
|
|||
"apollo-link-error": "^1.1.5",
|
||||
"apollo-link-schema": "^1.1.1",
|
||||
"apollo-link-state": "^0.4.2",
|
||||
"apollo-link-watched-mutation": "^0.1.0",
|
||||
"apollo-server": "2.3.3",
|
||||
"apollo-server-express": "2.3.3",
|
||||
"babel-runtime": "^6.26.0",
|
||||
|
@ -70,14 +71,14 @@
|
|||
"moment": "^2.13.0",
|
||||
"prop-types": "^15.6.0",
|
||||
"qs": "^6.6.0",
|
||||
"react": "^16.2.0",
|
||||
"react": "^16.4.1",
|
||||
"react-addons-pure-render-mixin": "^15.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-cookie": "^2.1.4",
|
||||
"react-datetime": "^2.11.1",
|
||||
"react-dom": "^16.2.0",
|
||||
"react-dom": "^16.4.1",
|
||||
"react-dropzone": "^8.0.3",
|
||||
"react-helmet": "^5.1.3",
|
||||
"react-intl": "^2.1.3",
|
||||
|
|
|
@ -6,7 +6,7 @@ import { STATES } from '../../helpers.js';
|
|||
|
||||
class AccountsEnrollAccount extends PureComponent {
|
||||
componentDidMount() {
|
||||
const token = this.props.params.token;
|
||||
const token = this.props.match.params.token;
|
||||
Accounts._loginButtonsSession.set('enrollAccountToken', token);
|
||||
}
|
||||
|
||||
|
|
|
@ -265,9 +265,20 @@ export class AccountsLoginFormInner extends TrackerComponent {
|
|||
}
|
||||
|
||||
fields() {
|
||||
const loginFields = [];
|
||||
let loginFields = [];
|
||||
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) {
|
||||
loginFields.push({
|
||||
label: 'No login service added, i.e. accounts-password',
|
||||
|
@ -766,6 +777,13 @@ export class AccountsLoginFormInner extends TrackerComponent {
|
|||
onSubmitHook
|
||||
} = this.state;
|
||||
|
||||
// add extra fields to options
|
||||
if (this.props.extraFields) {
|
||||
this.props.extraFields.forEach(({ id })=> {
|
||||
options[id] = this.state[id];
|
||||
});
|
||||
}
|
||||
|
||||
const self = this;
|
||||
|
||||
let error = false;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
Package.describe({
|
||||
name: 'vulcan:accounts',
|
||||
version: '1.12.17',
|
||||
version: '1.13.0',
|
||||
summary: 'Accounts UI for React in Meteor 1.3+',
|
||||
git: 'https://github.com/studiointeract/accounts-ui',
|
||||
documentation: 'README.md',
|
||||
|
@ -9,7 +9,7 @@ Package.describe({
|
|||
Package.onUse(function(api) {
|
||||
api.versionsFrom('1.6.1');
|
||||
|
||||
api.use('vulcan:core@1.12.17');
|
||||
api.use('vulcan:core@1.13.0');
|
||||
|
||||
api.use('ecmascript');
|
||||
api.use('tracker');
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
Package.describe({
|
||||
name: 'vulcan:admin',
|
||||
summary: 'Vulcan components package',
|
||||
version: '1.12.17',
|
||||
version: '1.13.0',
|
||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||
});
|
||||
|
||||
|
@ -12,7 +12,7 @@ Package.onUse(function(api) {
|
|||
'fourseven:scss@4.10.0',
|
||||
'dynamic-import@0.1.1',
|
||||
// Vulcan packages
|
||||
'vulcan:core@1.12.17',
|
||||
'vulcan:core@1.13.0',
|
||||
]);
|
||||
|
||||
api.mainModule('lib/server/main.js', 'server');
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
Package.describe({
|
||||
name: 'vulcan:cloudinary',
|
||||
summary: 'Vulcan file upload package.',
|
||||
version: '1.12.17',
|
||||
version: '1.13.0',
|
||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
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/server/main.js', 'server');
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Components, registerComponent } from 'meteor/vulcan:lib';
|
|||
import React from 'react';
|
||||
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
|
||||
label={label || intl.formatMessage({ id: 'datatable.edit' })}
|
||||
component={
|
||||
|
@ -12,7 +12,7 @@ const EditButton = ({ style = 'primary', label, size, showId, modalProps, ...pro
|
|||
}
|
||||
modalProps={modalProps}
|
||||
>
|
||||
<Components.EditForm {...props} />
|
||||
<Components.EditForm {...props} formProps={formProps}/>
|
||||
</Components.ModalTrigger>
|
||||
);
|
||||
|
||||
|
@ -29,7 +29,7 @@ registerComponent('EditButton', EditButton);
|
|||
EditForm Component
|
||||
|
||||
*/
|
||||
const EditForm = ({ closeModal, successCallback, removeSuccessCallback, ...props }) => {
|
||||
const EditForm = ({ closeModal, successCallback, removeSuccessCallback, formProps, ...props }) => {
|
||||
|
||||
const success = successCallback
|
||||
? document => {
|
||||
|
@ -46,7 +46,7 @@ const EditForm = ({ closeModal, successCallback, removeSuccessCallback, ...props
|
|||
: closeModal;
|
||||
|
||||
return (
|
||||
<Components.SmartForm successCallback={success} removeSuccessCallback={remove} {...props} />
|
||||
<Components.SmartForm successCallback={success} removeSuccessCallback={remove} {...formProps} {...props} />
|
||||
);
|
||||
};
|
||||
registerComponent('EditForm', EditForm);
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Components, registerComponent } from 'meteor/vulcan:lib';
|
|||
import React from 'react';
|
||||
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
|
||||
label={label || intl.formatMessage({ id: 'datatable.new' })}
|
||||
component={
|
||||
|
@ -11,7 +11,7 @@ const NewButton = ({ collection, size, label, style = 'primary', ...props }, { i
|
|||
</Components.Button>
|
||||
}
|
||||
>
|
||||
<Components.NewForm collection={collection} {...props} />
|
||||
<Components.NewForm collection={collection} formProps={formProps} {...props} />
|
||||
</Components.ModalTrigger>
|
||||
);
|
||||
|
||||
|
@ -28,7 +28,7 @@ registerComponent('NewButton', NewButton);
|
|||
NewForm Component
|
||||
|
||||
*/
|
||||
const NewForm = ({ closeModal, successCallback, ...props }) => {
|
||||
const NewForm = ({ closeModal, successCallback, formProps, ...props }) => {
|
||||
|
||||
const success = successCallback
|
||||
? document => {
|
||||
|
@ -37,6 +37,6 @@ const NewForm = ({ closeModal, successCallback, ...props }) => {
|
|||
}
|
||||
: closeModal;
|
||||
|
||||
return <Components.SmartForm successCallback={success} {...props} />;
|
||||
return <Components.SmartForm successCallback={success} {...formProps} {...props} />;
|
||||
};
|
||||
registerComponent('NewForm', NewForm);
|
||||
|
|
|
@ -28,6 +28,135 @@ import get from 'lodash/get';
|
|||
|
||||
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) {
|
||||
let typeName, collectionName, mutationOptions;
|
||||
|
||||
|
@ -46,17 +175,16 @@ export function getDefaultMutations(options) {
|
|||
// register callbacks for documentation purposes
|
||||
registerCollectionCallbacks(typeName, mutationOptions);
|
||||
|
||||
const multiResolverName = Utils.camelCaseify(Utils.pluralize(typeName));
|
||||
const multiQueryName = `multi${typeName}Query`;
|
||||
const mutations = {};
|
||||
|
||||
if (mutationOptions.create) {
|
||||
// mutation for inserting a new document
|
||||
|
||||
const mutationName = `create${typeName}`;
|
||||
const mutationName = getCreateMutationName(typeName);
|
||||
|
||||
const createMutation = {
|
||||
description: `Mutation for creating new ${typeName} documents`,
|
||||
name: mutationName,
|
||||
|
||||
// check function called on a user to see if they can perform the operation
|
||||
check(user, document) {
|
||||
|
@ -99,51 +227,16 @@ export function getDefaultMutations(options) {
|
|||
mutations.create = createMutation;
|
||||
// OpenCRUD backwards compatibility
|
||||
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) {
|
||||
// mutation for editing a specific document
|
||||
|
||||
const mutationName = `update${typeName}`;
|
||||
const mutationName = getUpdateMutationName(typeName);
|
||||
|
||||
const updateMutation = {
|
||||
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(user, document) {
|
||||
|
@ -212,56 +305,13 @@ export function getDefaultMutations(options) {
|
|||
// OpenCRUD backwards compatibility
|
||||
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) {
|
||||
// mutation for upserting a specific document
|
||||
const mutationName = getUpsertMutationName(typeName);
|
||||
mutations.upsert = {
|
||||
description: `Mutation for upserting a ${typeName} document`,
|
||||
name: mutationName,
|
||||
|
||||
async mutation(root, { selector, data }, context) {
|
||||
const collection = context[collectionName];
|
||||
|
@ -286,10 +336,11 @@ export function getDefaultMutations(options) {
|
|||
if (mutationOptions.delete) {
|
||||
// mutation for removing a specific document (same checks as edit mutation)
|
||||
|
||||
const mutationName = `delete${typeName}`;
|
||||
const mutationName = getDeleteMutationName(typeName);
|
||||
|
||||
const deleteMutation = {
|
||||
description: `Mutation for deleting a ${typeName} document`,
|
||||
name: mutationName,
|
||||
|
||||
check(user, document) {
|
||||
// OpenCRUD backwards compatibility
|
||||
|
@ -351,26 +402,10 @@ export function getDefaultMutations(options) {
|
|||
// OpenCRUD backwards compatibility
|
||||
mutations.remove = deleteMutation;
|
||||
|
||||
/*
|
||||
|
||||
Handle post-mutation updates of the client cache
|
||||
|
||||
*/
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
if (Meteor.isClient) {
|
||||
registerWatchedMutations(mutations, typeName);
|
||||
}
|
||||
|
||||
return mutations;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
Package.describe({
|
||||
name: 'vulcan:core',
|
||||
summary: 'Vulcan core package',
|
||||
version: '1.12.17',
|
||||
version: '1.13.0',
|
||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||
});
|
||||
|
||||
|
@ -9,14 +9,14 @@ Package.onUse(function(api) {
|
|||
api.versionsFrom('1.6.1');
|
||||
|
||||
api.use([
|
||||
'vulcan:lib@1.12.17',
|
||||
'vulcan:i18n@1.12.17',
|
||||
'vulcan:users@1.12.17',
|
||||
'vulcan:routing@1.12.17',
|
||||
'vulcan:debug@1.12.17',
|
||||
'vulcan:lib@1.13.0',
|
||||
'vulcan:i18n@1.13.0',
|
||||
'vulcan:users@1.13.0',
|
||||
'vulcan:routing@1.13.0',
|
||||
'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/client/main.js', 'client');
|
||||
|
@ -25,4 +25,5 @@ Package.onUse(function(api) {
|
|||
Package.onTest(function(api) {
|
||||
api.use(['ecmascript', 'meteortesting:mocha', 'vulcan:test', 'vulcan:core']);
|
||||
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 './components.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({
|
||||
name: 'vulcan:debug',
|
||||
summary: 'Vulcan debug package',
|
||||
version: '1.12.17',
|
||||
version: '1.13.0',
|
||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||
debugOnly: true,
|
||||
});
|
||||
|
@ -15,8 +15,8 @@ Package.onUse(function(api) {
|
|||
|
||||
// Vulcan packages
|
||||
|
||||
'vulcan:lib@1.12.17',
|
||||
'vulcan:email@1.12.17',
|
||||
'vulcan:lib@1.13.0',
|
||||
'vulcan:email@1.13.0',
|
||||
]);
|
||||
|
||||
api.addFiles(['lib/stylesheets/debug.scss'], ['client']);
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
Package.describe({
|
||||
name: 'vulcan:email',
|
||||
summary: 'Vulcan email package',
|
||||
version: '1.12.17',
|
||||
version: '1.13.0',
|
||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
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/client.js', 'client');
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
Package.describe({
|
||||
name: 'vulcan:embed',
|
||||
summary: 'Vulcan Embed package',
|
||||
version: '1.12.17',
|
||||
version: '1.13.0',
|
||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
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']);
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
Package.describe({
|
||||
name: 'vulcan:errors-sentry',
|
||||
summary: 'Vulcan Sentry error tracking package',
|
||||
version: '1.12.17',
|
||||
version: '1.13.0',
|
||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
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/client/main.js', 'client');
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
Package.describe({
|
||||
name: 'vulcan:errors',
|
||||
summary: 'Vulcan error tracking package',
|
||||
version: '1.12.17',
|
||||
version: '1.13.0',
|
||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
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/client/main.js', 'client');
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
Package.describe({
|
||||
name: 'vulcan:events-ga',
|
||||
summary: 'Vulcan Google Analytics event tracking package',
|
||||
version: '1.12.17',
|
||||
version: '1.13.0',
|
||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
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/client/main.js', 'client');
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
Package.describe({
|
||||
name: 'vulcan:events-intercom',
|
||||
summary: 'Vulcan Intercom integration package.',
|
||||
version: '1.12.17',
|
||||
version: '1.13.0',
|
||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
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/server/main.js', 'server');
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
Package.describe({
|
||||
name: 'vulcan:events-internal',
|
||||
summary: 'Vulcan internal event tracking package',
|
||||
version: '1.12.17',
|
||||
version: '1.13.0',
|
||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
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/client/main.js', 'client');
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
Package.describe({
|
||||
name: 'vulcan:events-segment',
|
||||
summary: 'Vulcan Segment',
|
||||
version: '1.12.17',
|
||||
version: '1.13.0',
|
||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
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/client/main.js', 'client');
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
Package.describe({
|
||||
name: 'vulcan:events',
|
||||
summary: 'Vulcan event tracking package',
|
||||
version: '1.12.17',
|
||||
version: '1.13.0',
|
||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
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/client/main.js', 'client');
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
Package.describe({
|
||||
name: 'vulcan:forms-tags',
|
||||
summary: 'Vulcan tag input package',
|
||||
version: '1.12.17',
|
||||
version: '1.13.0',
|
||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
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']);
|
||||
});
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
Package.describe({
|
||||
name: 'vulcan:forms-upload',
|
||||
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',
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
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');
|
||||
|
||||
|
|
|
@ -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 = () => {
|
||||
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
|
||||
handlePageLeave = event => {
|
||||
/**
|
||||
* Same for browser closing
|
||||
*
|
||||
* 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()) {
|
||||
const message = this.context.intl.formatMessage({
|
||||
id: 'forms.confirm_discard',
|
||||
|
@ -765,41 +833,6 @@ class SmartForm extends Component {
|
|||
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
|
||||
|
|
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/FormElement.jsx';
|
||||
import '../components/FormErrors.jsx';
|
||||
import '../components/FormError.jsx';
|
||||
import '../components/FormComponent.jsx';
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
Package.describe({
|
||||
name: 'vulcan:forms',
|
||||
summary: 'Form containers for React',
|
||||
version: '1.12.17',
|
||||
version: '1.13.0',
|
||||
git: 'https://github.com/meteor-utilities/react-form-containers.git',
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
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/server/main.js', ['server']);
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
Package.describe({
|
||||
name: 'vulcan:i18n-en-us',
|
||||
summary: 'Vulcan i18n package (en_US)',
|
||||
version: '1.12.17',
|
||||
version: '1.13.0',
|
||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
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']);
|
||||
});
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
Package.describe({
|
||||
name: 'vulcan:i18n-es-es',
|
||||
summary: 'Vulcan i18n package (es_ES)',
|
||||
version: '1.12.17',
|
||||
version: '1.13.0',
|
||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
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']);
|
||||
});
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
Package.describe({
|
||||
name: 'vulcan:i18n-fr-fr',
|
||||
summary: 'Vulcan i18n package (fr_FR)',
|
||||
version: '1.12.17',
|
||||
version: '1.13.0',
|
||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
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']);
|
||||
});
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
Package.describe({
|
||||
name: 'vulcan:i18n',
|
||||
summary: 'i18n client polyfill',
|
||||
version: '1.12.17',
|
||||
version: '1.13.0',
|
||||
git: 'https://github.com/VulcanJS/Vulcan',
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
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/client/main.js', 'client');
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { onError } from 'apollo-link-error';
|
||||
|
||||
const locationsToStr = (locations=[]) => locations.map(({column, line}) => `line ${line}, col ${column}`).join(';');
|
||||
const errorLink = onError(({ graphQLErrors, networkError }) => {
|
||||
if (graphQLErrors)
|
||||
graphQLErrors.map(({ message, locations, path }) => {
|
||||
// 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) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
|
|
@ -9,7 +9,7 @@ import SimpleSchema from 'simpl-schema';
|
|||
Vulcan = {};
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
Vulcan.VERSION = '1.12.17';
|
||||
Vulcan.VERSION = '1.13.0';
|
||||
|
||||
// ------------------------------------- Schemas -------------------------------- //
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ const getFirstWord = input => {
|
|||
|
||||
Parse a GraphQL error message
|
||||
|
||||
TODO: check if still useful?
|
||||
|
||||
Sample message:
|
||||
|
||||
"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 => {
|
||||
|
||||
if (!message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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');
|
||||
|
||||
|
@ -52,6 +59,7 @@ const parseErrorMessage = message => {
|
|||
});
|
||||
return fieldErrors;
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
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
|
||||
- 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 => {
|
||||
|
||||
// 1. by default, return raw error wrapped in array
|
||||
let errors = [error];
|
||||
const graphQLErrors = error.graphQLErrors;
|
||||
|
||||
// error thrown using new ApolloError
|
||||
const apolloErrors = get(graphQLErrors, '0.extensions.exception.data.errors');
|
||||
|
||||
// regular server error (with schema stitching)
|
||||
const regularErrors = get(graphQLErrors, '0.extensions.exception.errors');
|
||||
|
||||
return apolloErrors || regularErrors || graphQLErrors;
|
||||
|
||||
// if this is one or more GraphQL errors, extract and convert them
|
||||
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
||||
// get first graphQL error (see https://github.com/thebigredgeek/apollo-errors/issues/12)
|
||||
const graphQLError = error.graphQLErrors[0];
|
||||
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 { default as initGraphQL } from './initGraphQL';
|
||||
|
|
|
@ -75,7 +75,8 @@ const initGraphQL = () => {
|
|||
resolvers: GraphQLSchema.resolvers,
|
||||
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.executableSchema = mergedSchema;
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
const { onStart } = require('./apollo_server2');
|
||||
const { onStart } = require('./apollo_server');
|
||||
// createApolloServer when server startup
|
||||
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 => {
|
||||
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 './source_version.js';
|
||||
|
||||
export * from './apollo-server/settings.js';
|
||||
|
||||
import './apollo-server/startup';
|
||||
|
|
|
@ -80,7 +80,7 @@ export const createMutator = async ({
|
|||
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
|
||||
|
||||
*/
|
||||
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
|
||||
|
||||
*/
|
||||
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 => {
|
||||
debug(`// ${p}: `, properties[p]);
|
||||
});
|
||||
|
|
|
@ -93,9 +93,10 @@ Meteor.startup(() => {
|
|||
Collections.forEach(collection => {
|
||||
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 result = await runQuery(query, { input: { selector: { documentId } } }, context);
|
||||
const result = await runQuery(query, { input: { selector } }, context);
|
||||
return result.data[Utils.camelCaseify(typeName)].result;
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
Package.describe({
|
||||
name: 'vulcan:lib',
|
||||
summary: 'Vulcan libraries.',
|
||||
version: '1.12.17',
|
||||
version: '1.13.0',
|
||||
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 { GraphQLSchema } from '../../lib/modules/graphql';
|
||||
import expect from 'expect';
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
Package.describe({
|
||||
name: 'vulcan:newsletter',
|
||||
summary: 'Vulcan email newsletter package',
|
||||
version: '1.12.17',
|
||||
version: '1.13.0',
|
||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
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/client/main.js', 'client');
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
Package.describe({
|
||||
name: 'vulcan:payments',
|
||||
summary: 'Vulcan payments package',
|
||||
version: '1.12.17',
|
||||
version: '1.13.0',
|
||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
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/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({
|
||||
name: 'vulcan:routing',
|
||||
summary: 'Vulcan router package',
|
||||
version: '1.12.17',
|
||||
version: '1.13.0',
|
||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
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/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({
|
||||
name: 'vulcan:subscribe',
|
||||
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',
|
||||
});
|
||||
|
||||
|
@ -9,11 +9,11 @@ Package.onUse(function(api) {
|
|||
api.versionsFrom('1.6.1');
|
||||
|
||||
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
|
||||
]);
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
|
|
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({
|
||||
name: 'vulcan:ui-bootstrap',
|
||||
summary: 'Vulcan Bootstrap UI components.',
|
||||
version: '1.12.17',
|
||||
version: '1.13.0',
|
||||
git: 'https://github.com/VulcanJS/Vulcan.git',
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
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');
|
||||
|
||||
|
|
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