diff --git a/.eslintrc b/.eslintrc
index e175ad797..8254b5ac7 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -54,6 +54,7 @@
"single",
"avoid-escape"
],
+ "react/display-name": 1,
"react/prop-types": 0,
"semi": [1, "always"]
},
diff --git a/package-lock.json b/package-lock.json
index adbaeba4d..cdf6b7e43 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 7a34e65e9..62eaf8b77 100644
--- a/package.json
+++ b/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",
diff --git a/packages/vulcan-accounts/imports/ui/components/EnrollAccount.jsx b/packages/vulcan-accounts/imports/ui/components/EnrollAccount.jsx
index 012f65456..015f8c6b2 100644
--- a/packages/vulcan-accounts/imports/ui/components/EnrollAccount.jsx
+++ b/packages/vulcan-accounts/imports/ui/components/EnrollAccount.jsx
@@ -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);
}
diff --git a/packages/vulcan-accounts/imports/ui/components/LoginFormInner.jsx b/packages/vulcan-accounts/imports/ui/components/LoginFormInner.jsx
index c32117135..eb114416c 100644
--- a/packages/vulcan-accounts/imports/ui/components/LoginFormInner.jsx
+++ b/packages/vulcan-accounts/imports/ui/components/LoginFormInner.jsx
@@ -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;
diff --git a/packages/vulcan-accounts/package.js b/packages/vulcan-accounts/package.js
index fe2016336..28d45d1f5 100755
--- a/packages/vulcan-accounts/package.js
+++ b/packages/vulcan-accounts/package.js
@@ -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');
diff --git a/packages/vulcan-admin/package.js b/packages/vulcan-admin/package.js
index 6a4849c03..160a26f0f 100644
--- a/packages/vulcan-admin/package.js
+++ b/packages/vulcan-admin/package.js
@@ -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');
diff --git a/packages/vulcan-cloudinary/package.js b/packages/vulcan-cloudinary/package.js
index 26e2599d2..bdba9e2ac 100644
--- a/packages/vulcan-cloudinary/package.js
+++ b/packages/vulcan-cloudinary/package.js
@@ -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');
diff --git a/packages/vulcan-core/lib/modules/components/EditButton.jsx b/packages/vulcan-core/lib/modules/components/EditButton.jsx
index 7f15bef7d..9e0d6e28a 100644
--- a/packages/vulcan-core/lib/modules/components/EditButton.jsx
+++ b/packages/vulcan-core/lib/modules/components/EditButton.jsx
@@ -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 }) => (
-
+
);
@@ -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 (
-
+
);
};
registerComponent('EditForm', EditForm);
diff --git a/packages/vulcan-core/lib/modules/components/NewButton.jsx b/packages/vulcan-core/lib/modules/components/NewButton.jsx
index 262eba4af..e966577d7 100644
--- a/packages/vulcan-core/lib/modules/components/NewButton.jsx
+++ b/packages/vulcan-core/lib/modules/components/NewButton.jsx
@@ -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 }) => (
}
>
-
+
);
@@ -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 ;
+ return ;
};
registerComponent('NewForm', NewForm);
diff --git a/packages/vulcan-core/lib/modules/default_mutations.js b/packages/vulcan-core/lib/modules/default_mutations.js
index 6e7dc3081..c1721e761 100644
--- a/packages/vulcan-core/lib/modules/default_mutations.js
+++ b/packages/vulcan-core/lib/modules/default_mutations.js
@@ -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) {
@@ -160,13 +253,13 @@ export function getDefaultMutations(options) {
// OpenCRUD backwards compatibility
return Users.owns(user, document)
? Users.canDo(user, [
- `${typeName.toLowerCase()}.update.own`,
- `${collectionName.toLowerCase()}.edit.own`,
- ])
+ `${typeName.toLowerCase()}.update.own`,
+ `${collectionName.toLowerCase()}.edit.own`,
+ ])
: Users.canDo(user, [
- `${typeName.toLowerCase()}.update.all`,
- `${collectionName.toLowerCase()}.edit.all`,
- ]);
+ `${typeName.toLowerCase()}.update.all`,
+ `${collectionName.toLowerCase()}.edit.all`,
+ ]);
},
async mutation(root, { selector, data }, context) {
@@ -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
@@ -302,13 +353,13 @@ export function getDefaultMutations(options) {
// OpenCRUD backwards compatibility
return Users.owns(user, document)
? Users.canDo(user, [
- `${typeName.toLowerCase()}.delete.own`,
- `${collectionName.toLowerCase()}.remove.own`,
- ])
+ `${typeName.toLowerCase()}.delete.own`,
+ `${collectionName.toLowerCase()}.remove.own`,
+ ])
: Users.canDo(user, [
- `${typeName.toLowerCase()}.delete.all`,
- `${collectionName.toLowerCase()}.remove.all`,
- ]);
+ `${typeName.toLowerCase()}.delete.all`,
+ `${collectionName.toLowerCase()}.remove.all`,
+ ]);
},
async mutation(root, { selector }, context) {
@@ -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;
diff --git a/packages/vulcan-core/package.js b/packages/vulcan-core/package.js
index 3868867d6..1bb3b84ad 100644
--- a/packages/vulcan-core/package.js
+++ b/packages/vulcan-core/package.js
@@ -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']);
});
diff --git a/packages/vulcan-core/test/client/index.js b/packages/vulcan-core/test/client/index.js
new file mode 100644
index 000000000..c49a4a470
--- /dev/null
+++ b/packages/vulcan-core/test/client/index.js
@@ -0,0 +1 @@
+import './mutations.test';
\ No newline at end of file
diff --git a/packages/vulcan-core/test/client/mutations.test.js b/packages/vulcan-core/test/client/mutations.test.js
new file mode 100644
index 000000000..344cef32b
--- /dev/null
+++ b/packages/vulcan-core/test/client/mutations.test.js
@@ -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);
+ });
+});
\ No newline at end of file
diff --git a/packages/vulcan-core/test/index.js b/packages/vulcan-core/test/index.js
index 1fe7eec6d..d6ee0c346 100644
--- a/packages/vulcan-core/test/index.js
+++ b/packages/vulcan-core/test/index.js
@@ -1,3 +1,4 @@
+import './mutations.test';
import './resolvers.test';
import './components.test';
import './containers.test';
diff --git a/packages/vulcan-core/test/mutations.test.js b/packages/vulcan-core/test/mutations.test.js
new file mode 100644
index 000000000..d9c0f2687
--- /dev/null
+++ b/packages/vulcan-core/test/mutations.test.js
@@ -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();
+ });
+
+});
\ No newline at end of file
diff --git a/packages/vulcan-debug/package.js b/packages/vulcan-debug/package.js
index 99a5f29c4..1e6153d27 100644
--- a/packages/vulcan-debug/package.js
+++ b/packages/vulcan-debug/package.js
@@ -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']);
diff --git a/packages/vulcan-email/package.js b/packages/vulcan-email/package.js
index a8f1f0eee..0b583e12c 100644
--- a/packages/vulcan-email/package.js
+++ b/packages/vulcan-email/package.js
@@ -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');
diff --git a/packages/vulcan-embed/package.js b/packages/vulcan-embed/package.js
index 7ff649f27..6d42f14e7 100644
--- a/packages/vulcan-embed/package.js
+++ b/packages/vulcan-embed/package.js
@@ -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']);
diff --git a/packages/vulcan-errors-sentry/package.js b/packages/vulcan-errors-sentry/package.js
index 4a74bd211..f8a669c41 100755
--- a/packages/vulcan-errors-sentry/package.js
+++ b/packages/vulcan-errors-sentry/package.js
@@ -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');
diff --git a/packages/vulcan-errors/package.js b/packages/vulcan-errors/package.js
index ece2e44c5..550c34a54 100644
--- a/packages/vulcan-errors/package.js
+++ b/packages/vulcan-errors/package.js
@@ -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');
diff --git a/packages/vulcan-events-ga/package.js b/packages/vulcan-events-ga/package.js
index e220d3194..13dbf655d 100644
--- a/packages/vulcan-events-ga/package.js
+++ b/packages/vulcan-events-ga/package.js
@@ -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');
diff --git a/packages/vulcan-events-intercom/package.js b/packages/vulcan-events-intercom/package.js
index 8d8a7cdcb..eaf6af7e4 100644
--- a/packages/vulcan-events-intercom/package.js
+++ b/packages/vulcan-events-intercom/package.js
@@ -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');
diff --git a/packages/vulcan-events-internal/package.js b/packages/vulcan-events-internal/package.js
index 68fedb9e9..1d8d8f6e1 100644
--- a/packages/vulcan-events-internal/package.js
+++ b/packages/vulcan-events-internal/package.js
@@ -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');
diff --git a/packages/vulcan-events-segment/package.js b/packages/vulcan-events-segment/package.js
index 06e413d9b..a455202ee 100644
--- a/packages/vulcan-events-segment/package.js
+++ b/packages/vulcan-events-segment/package.js
@@ -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');
diff --git a/packages/vulcan-events/package.js b/packages/vulcan-events/package.js
index 5c59a1e5a..469c906b2 100644
--- a/packages/vulcan-events/package.js
+++ b/packages/vulcan-events/package.js
@@ -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');
diff --git a/packages/vulcan-forms-tags/package.js b/packages/vulcan-forms-tags/package.js
index 6da54d2f8..4cf501499 100644
--- a/packages/vulcan-forms-tags/package.js
+++ b/packages/vulcan-forms-tags/package.js
@@ -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']);
});
diff --git a/packages/vulcan-forms-upload/package.js b/packages/vulcan-forms-upload/package.js
index 2cafe0aa8..3269975de 100755
--- a/packages/vulcan-forms-upload/package.js
+++ b/packages/vulcan-forms-upload/package.js
@@ -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');
diff --git a/packages/vulcan-forms/lib/components/Form.jsx b/packages/vulcan-forms/lib/components/Form.jsx
index 3b4659f36..db2201f53 100644
--- a/packages/vulcan-forms/lib/components/Form.jsx
+++ b/packages/vulcan-forms/lib/components/Form.jsx
@@ -66,7 +66,7 @@ import { callbackProps } from './propTypes';
// props that should trigger a form reset
const RESET_PROPS = [
- 'collection', 'collectionName', 'typeName', 'document', 'schema', 'currentUser',
+ 'collection', 'collectionName', 'typeName', 'document', 'schema', 'currentUser',
'fields', 'removeFields',
'prefilledProps' // TODO: prefilledProps should be merged instead?
];
@@ -104,17 +104,17 @@ const getInitialStateFromProps = nextProps => {
nextProps.prefilledProps,
nextProps.document
);
-
+
//if minCount is specified, go ahead and create empty nested documents
Object.keys(convertedSchema).forEach(key => {
let minCount = convertedSchema[key].minCount;
- if(minCount) {
+ if (minCount) {
initialDocument[key] = initialDocument[key] || [];
- while(initialDocument[key].length < minCount)
+ while (initialDocument[key].length < minCount)
initialDocument[key].push({});
}
});
-
+
// remove all instances of the `__typename` property from document
Utils.removeProperty(initialDocument, '__typename');
@@ -154,7 +154,7 @@ class SmartForm extends Component {
};
}
- defaultValues = {};
+ defaultValues = {};
submitFormCallbacks = [];
successFormCallbacks = [];
@@ -266,7 +266,7 @@ class SmartForm extends Component {
});
// run data object through submitForm callbacks
- data = runCallbacks({ callbacks: this.submitFormCallbacks, iterator: data, properties: { form: this }});
+ data = runCallbacks({ callbacks: this.submitFormCallbacks, iterator: data, properties: { form: this } });
return data;
};
@@ -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
@@ -899,7 +932,7 @@ class SmartForm extends Component {
}
// run document through mutation success callbacks
- document = runCallbacks({ callbacks: this.successFormCallbacks, iterator: document, properties: { form: this }});
+ document = runCallbacks({ callbacks: this.successFormCallbacks, iterator: document, properties: { form: this } });
// run success callback if it exists
if (this.props.successCallback) this.props.successCallback(document, { form: this });
@@ -915,7 +948,7 @@ class SmartForm extends Component {
console.log(error);
// run mutation failure callbacks on error, we do not allow the callbacks to change the error
- runCallbacks({ callbacks: this.failureFormCallbacks, iterator: error, properties: { error, form: this }});
+ runCallbacks({ callbacks: this.failureFormCallbacks, iterator: error, properties: { error, form: this } });
if (!_.isEmpty(error)) {
// add error to state
@@ -937,7 +970,7 @@ class SmartForm extends Component {
submitForm = event => {
event && event.preventDefault();
-
+
// if form is disabled (there is already a submit handler running) don't do anything
if (this.state.disabled) {
return;
@@ -1003,7 +1036,7 @@ class SmartForm extends Component {
}
};
-
+
// --------------------------------------------------------------------- //
// ------------------------- Props to Pass ----------------------------- //
// --------------------------------------------------------------------- //
@@ -1045,15 +1078,15 @@ class SmartForm extends Component {
cancelCallback: this.props.cancelCallback,
revertCallback: this.props.revertCallback,
document: this.getDocument(),
- deleteDocument:
+ deleteDocument:
(this.getFormType() === 'edit' &&
this.props.showRemove &&
this.deleteDocument) ||
null,
- collectionName:this.props.collectionName,
- currentValues:this.state.currentValues,
- deletedValues:this.state.deletedValues,
- errors:this.state.errors,
+ collectionName: this.props.collectionName,
+ currentValues: this.state.currentValues,
+ deletedValues: this.state.deletedValues,
+ errors: this.state.errors,
});
// --------------------------------------------------------------------- //
diff --git a/packages/vulcan-forms/lib/components/FormElement.jsx b/packages/vulcan-forms/lib/components/FormElement.jsx
new file mode 100644
index 000000000..fc0a619df
--- /dev/null
+++ b/packages/vulcan-forms/lib/components/FormElement.jsx
@@ -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
;
+ }
+}
+registerComponent({
+ name:'FormElement',
+ component: FormElement
+});
\ No newline at end of file
diff --git a/packages/vulcan-forms/lib/modules/components.js b/packages/vulcan-forms/lib/modules/components.js
index f820acd89..9d6c649e7 100644
--- a/packages/vulcan-forms/lib/modules/components.js
+++ b/packages/vulcan-forms/lib/modules/components.js
@@ -1,4 +1,5 @@
import '../components/FieldErrors.jsx';
+import '../components/FormElement.jsx';
import '../components/FormErrors.jsx';
import '../components/FormError.jsx';
import '../components/FormComponent.jsx';
diff --git a/packages/vulcan-forms/package.js b/packages/vulcan-forms/package.js
index b42014aca..5fdfc9e36 100644
--- a/packages/vulcan-forms/package.js
+++ b/packages/vulcan-forms/package.js
@@ -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']);
diff --git a/packages/vulcan-i18n-en-us/package.js b/packages/vulcan-i18n-en-us/package.js
index 202171609..cc863b098 100644
--- a/packages/vulcan-i18n-en-us/package.js
+++ b/packages/vulcan-i18n-en-us/package.js
@@ -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']);
});
diff --git a/packages/vulcan-i18n-es-es/package.js b/packages/vulcan-i18n-es-es/package.js
index a742182d6..816480e2e 100644
--- a/packages/vulcan-i18n-es-es/package.js
+++ b/packages/vulcan-i18n-es-es/package.js
@@ -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']);
});
diff --git a/packages/vulcan-i18n-fr-fr/package.js b/packages/vulcan-i18n-fr-fr/package.js
index 2da96b969..253847c73 100644
--- a/packages/vulcan-i18n-fr-fr/package.js
+++ b/packages/vulcan-i18n-fr-fr/package.js
@@ -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']);
});
diff --git a/packages/vulcan-i18n/package.js b/packages/vulcan-i18n/package.js
index d9e3ae27f..a57538aa8 100644
--- a/packages/vulcan-i18n/package.js
+++ b/packages/vulcan-i18n/package.js
@@ -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');
diff --git a/packages/vulcan-lib/lib/client/apollo-client/links/error.js b/packages/vulcan-lib/lib/client/apollo-client/links/error.js
index cbe60daaf..57b6b670c 100644
--- a/packages/vulcan-lib/lib/client/apollo-client/links/error.js
+++ b/packages/vulcan-lib/lib/client/apollo-client/links/error.js
@@ -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
diff --git a/packages/vulcan-lib/lib/modules/config.js b/packages/vulcan-lib/lib/modules/config.js
index 4ac9c276b..67c8e8a63 100644
--- a/packages/vulcan-lib/lib/modules/config.js
+++ b/packages/vulcan-lib/lib/modules/config.js
@@ -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 -------------------------------- //
diff --git a/packages/vulcan-lib/lib/modules/errors.js b/packages/vulcan-lib/lib/modules/errors.js
index 3f350abea..24a74f20b 100644
--- a/packages/vulcan-lib/lib/modules/errors.js
+++ b/packages/vulcan-lib/lib/modules/errors.js
@@ -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');
- // 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;
+ // regular server error (with schema stitching)
+ const regularErrors = get(graphQLErrors, '0.extensions.exception.errors');
+
+ return apolloErrors || regularErrors || graphQLErrors;
+
};
diff --git a/packages/vulcan-lib/lib/server/apollo-server/apollo_server2.js b/packages/vulcan-lib/lib/server/apollo-server/apollo_server.js
similarity index 100%
rename from packages/vulcan-lib/lib/server/apollo-server/apollo_server2.js
rename to packages/vulcan-lib/lib/server/apollo-server/apollo_server.js
diff --git a/packages/vulcan-lib/lib/server/apollo-server/index.js b/packages/vulcan-lib/lib/server/apollo-server/index.js
index 502f69a5a..b7d314606 100644
--- a/packages/vulcan-lib/lib/server/apollo-server/index.js
+++ b/packages/vulcan-lib/lib/server/apollo-server/index.js
@@ -1,4 +1,4 @@
-export * from './apollo_server2';
+export * from './apollo_server';
export * from './settings';
export { default as initGraphQL } from './initGraphQL';
diff --git a/packages/vulcan-lib/lib/server/apollo-server/initGraphQL.js b/packages/vulcan-lib/lib/server/apollo-server/initGraphQL.js
index 789ccbe62..e0e73f481 100644
--- a/packages/vulcan-lib/lib/server/apollo-server/initGraphQL.js
+++ b/packages/vulcan-lib/lib/server/apollo-server/initGraphQL.js
@@ -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;
diff --git a/packages/vulcan-lib/lib/server/apollo-server/startup.js b/packages/vulcan-lib/lib/server/apollo-server/startup.js
index bf4fcb44a..61f12d86c 100644
--- a/packages/vulcan-lib/lib/server/apollo-server/startup.js
+++ b/packages/vulcan-lib/lib/server/apollo-server/startup.js
@@ -1,3 +1,3 @@
-const { onStart } = require('./apollo_server2');
+const { onStart } = require('./apollo_server');
// createApolloServer when server startup
Meteor.startup(onStart);
diff --git a/packages/vulcan-lib/lib/server/apollo_server.js b/packages/vulcan-lib/lib/server/apollo_server.js
deleted file mode 100644
index 03b5e8ef1..000000000
--- a/packages/vulcan-lib/lib/server/apollo_server.js
+++ /dev/null
@@ -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,
- });
-});
- */
diff --git a/packages/vulcan-lib/lib/server/errors.js b/packages/vulcan-lib/lib/server/errors.js
index cd7d70e8b..000b3870d 100644
--- a/packages/vulcan-lib/lib/server/errors.js
+++ b/packages/vulcan-lib/lib/server/errors.js
@@ -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);
};
diff --git a/packages/vulcan-lib/lib/server/main.js b/packages/vulcan-lib/lib/server/main.js
index 731dafd3f..181b49f18 100644
--- a/packages/vulcan-lib/lib/server/main.js
+++ b/packages/vulcan-lib/lib/server/main.js
@@ -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';
diff --git a/packages/vulcan-lib/lib/server/mutators.js b/packages/vulcan-lib/lib/server/mutators.js
index cf6d856fd..2a422264b 100644
--- a/packages/vulcan-lib/lib/server/mutators.js
+++ b/packages/vulcan-lib/lib/server/mutators.js
@@ -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]);
});
diff --git a/packages/vulcan-lib/lib/server/query.js b/packages/vulcan-lib/lib/server/query.js
index 15f60eb0e..ae573769b 100644
--- a/packages/vulcan-lib/lib/server/query.js
+++ b/packages/vulcan-lib/lib/server/query.js
@@ -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;
};
});
diff --git a/packages/vulcan-lib/package.js b/packages/vulcan-lib/package.js
index f2feff9e2..edcd4a486 100644
--- a/packages/vulcan-lib/package.js
+++ b/packages/vulcan-lib/package.js
@@ -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',
});
diff --git a/packages/vulcan-lib/test/server/apollo-server.test.js b/packages/vulcan-lib/test/server/apollo-server.test.js
index 53fe7bd2c..e6e1d41d7 100644
--- a/packages/vulcan-lib/test/server/apollo-server.test.js
+++ b/packages/vulcan-lib/test/server/apollo-server.test.js
@@ -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';
diff --git a/packages/vulcan-newsletter/package.js b/packages/vulcan-newsletter/package.js
index 08cf8b80a..4a0a59052 100644
--- a/packages/vulcan-newsletter/package.js
+++ b/packages/vulcan-newsletter/package.js
@@ -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');
diff --git a/packages/vulcan-payments/package.js b/packages/vulcan-payments/package.js
index 6c4c1adac..4e05f2f90 100644
--- a/packages/vulcan-payments/package.js
+++ b/packages/vulcan-payments/package.js
@@ -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');
diff --git a/packages/vulcan-redux/README.md b/packages/vulcan-redux/README.md
new file mode 100644
index 000000000..a6b3fd7ef
--- /dev/null
+++ b/packages/vulcan-redux/README.md
@@ -0,0 +1 @@
+Redux package.
\ No newline at end of file
diff --git a/packages/vulcan-routing/package.js b/packages/vulcan-routing/package.js
index dfc708ef5..06f65d56a 100644
--- a/packages/vulcan-routing/package.js
+++ b/packages/vulcan-routing/package.js
@@ -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');
diff --git a/packages/vulcan-styled-components/README.md b/packages/vulcan-styled-components/README.md
new file mode 100644
index 000000000..95409a7dc
--- /dev/null
+++ b/packages/vulcan-styled-components/README.md
@@ -0,0 +1 @@
+Styled components package.
\ No newline at end of file
diff --git a/packages/vulcan-subscribe/package.js b/packages/vulcan-subscribe/package.js
index 0951d7e07..1c21c1b8b 100644
--- a/packages/vulcan-subscribe/package.js
+++ b/packages/vulcan-subscribe/package.js
@@ -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,
});
diff --git a/packages/vulcan-test/README.md b/packages/vulcan-test/README.md
new file mode 100644
index 000000000..05740f98f
--- /dev/null
+++ b/packages/vulcan-test/README.md
@@ -0,0 +1 @@
+Test package.
\ No newline at end of file
diff --git a/packages/vulcan-ui-bootstrap/package.js b/packages/vulcan-ui-bootstrap/package.js
index dc984387f..69114840b 100644
--- a/packages/vulcan-ui-bootstrap/package.js
+++ b/packages/vulcan-ui-bootstrap/package.js
@@ -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');
diff --git a/packages/vulcan-ui-material/.gitignore b/packages/vulcan-ui-material/.gitignore
new file mode 100644
index 000000000..f8ca86071
--- /dev/null
+++ b/packages/vulcan-ui-material/.gitignore
@@ -0,0 +1,6 @@
+npm-debug.log
+node_modules
+.idea/workspace.xml
+
+### eslint-config
+.eslintrc
diff --git a/packages/vulcan-ui-material/.versions b/packages/vulcan-ui-material/.versions
new file mode 100644
index 000000000..5e93359dd
--- /dev/null
+++ b/packages/vulcan-ui-material/.versions
@@ -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
diff --git a/packages/vulcan-ui-material/accounts.css b/packages/vulcan-ui-material/accounts.css
new file mode 100644
index 000000000..73485abcf
--- /dev/null
+++ b/packages/vulcan-ui-material/accounts.css
@@ -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;
+}
+
diff --git a/packages/vulcan-ui-material/en_US.js b/packages/vulcan-ui-material/en_US.js
new file mode 100644
index 000000000..29711d64d
--- /dev/null
+++ b/packages/vulcan-ui-material/en_US.js
@@ -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}}',
+
+});
diff --git a/packages/vulcan-ui-material/forms.css b/packages/vulcan-ui-material/forms.css
new file mode 100644
index 000000000..219fee574
--- /dev/null
+++ b/packages/vulcan-ui-material/forms.css
@@ -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;
+}
diff --git a/packages/vulcan-ui-material/fr_FR.js b/packages/vulcan-ui-material/fr_FR.js
new file mode 100644
index 000000000..7ba3c6d2f
--- /dev/null
+++ b/packages/vulcan-ui-material/fr_FR.js
@@ -0,0 +1,7 @@
+import { addStrings } from 'meteor/vulcan:core';
+
+addStrings('fr', {
+ 'search.search': 'Recherche',
+ 'search.clear': 'Effacer la recherche',
+ 'modal.close': 'Fermer',
+});
diff --git a/packages/vulcan-ui-material/history.md b/packages/vulcan-ui-material/history.md
new file mode 100644
index 000000000..8613491ff
--- /dev/null
+++ b/packages/vulcan-ui-material/history.md
@@ -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
diff --git a/packages/vulcan-ui-material/lib/client/main.js b/packages/vulcan-ui-material/lib/client/main.js
new file mode 100644
index 000000000..dadfe15cf
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/client/main.js
@@ -0,0 +1,2 @@
+export * from '../modules/index';
+import './wrapWithMuiTheme';
diff --git a/packages/vulcan-ui-material/lib/client/wrapWithMuiTheme.jsx b/packages/vulcan-ui-material/lib/client/wrapWithMuiTheme.jsx
new file mode 100644
index 000000000..8b0c3baa7
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/client/wrapWithMuiTheme.jsx
@@ -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 (
+
+ {this.props.children}
+
+ );
+ }
+}
+
+registerComponent('ThemeProvider', ThemeProvider);
+
+function wrapWithMuiTheme(app) {
+ return (
+
+ {app}
+
+ );
+}
+
+
+addCallback('router.client.wrapper', wrapWithMuiTheme);
diff --git a/packages/vulcan-ui-material/lib/components/accounts/AccountsButton.jsx b/packages/vulcan-ui-material/lib/components/accounts/AccountsButton.jsx
new file mode 100755
index 000000000..d80dfe629
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/accounts/AccountsButton.jsx
@@ -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 (
+
+ );
+ }
+}
+
+
+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);
diff --git a/packages/vulcan-ui-material/lib/components/accounts/AccountsButtons.jsx b/packages/vulcan-ui-material/lib/components/accounts/AccountsButtons.jsx
new file mode 100755
index 000000000..dc55093d9
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/accounts/AccountsButtons.jsx
@@ -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 (
+
+ {Object.keys(buttons).map((id, i) =>
+
+ )}
+
+ );
+ }
+}
+
+
+AccountsButtons.propTypes = {
+ classes: PropTypes.object.isRequired,
+ buttons: PropTypes.object,
+ className: PropTypes.string,
+};
+
+
+AccountsButtons.displayName = 'AccountsButtons';
+
+
+replaceComponent('AccountsButtons', AccountsButtons, [withStyles, styles]);
diff --git a/packages/vulcan-ui-material/lib/components/accounts/AccountsField.jsx b/packages/vulcan-ui-material/lib/components/accounts/AccountsField.jsx
new file mode 100755
index 000000000..4ac940a51
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/accounts/AccountsField.jsx
@@ -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 {label}
;
+ }
+
+ 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 &&
+
+
+ { this.input = ref; }}
+ onChange={onChange}
+ placeholder={hint}
+ defaultValue={defaultValue}
+ autoComplete={autoComplete }
+ label={label}
+ autoFocus={autoFocus}
+ required={required}
+ error={!!message}
+ helperText={message && message.message}
+ fullWidth
+ />
+
+ );
+ }
+}
+
+
+AccountsField.propTypes = {
+ onChange: PropTypes.func,
+};
+
+
+replaceComponent('AccountsField', AccountsField);
diff --git a/packages/vulcan-ui-material/lib/components/accounts/AccountsFields.jsx b/packages/vulcan-ui-material/lib/components/accounts/AccountsFields.jsx
new file mode 100755
index 000000000..59309c9a3
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/accounts/AccountsFields.jsx
@@ -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 (
+
+ {
+ Object.keys(fields).map((id, i) =>
+
+ )
+ }
+
+ );
+ }
+}
+
+
+replaceComponent('AccountsFields', AccountsFields);
diff --git a/packages/vulcan-ui-material/lib/components/accounts/AccountsForm.jsx b/packages/vulcan-ui-material/lib/components/accounts/AccountsForm.jsx
new file mode 100755
index 000000000..af28fb37a
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/accounts/AccountsForm.jsx
@@ -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 (
+
+ );
+ }
+
+
+}
+
+
+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]);
diff --git a/packages/vulcan-ui-material/lib/components/accounts/AccountsPasswordOrService.jsx b/packages/vulcan-ui-material/lib/components/accounts/AccountsPasswordOrService.jsx
new file mode 100644
index 000000000..8f3a40667
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/accounts/AccountsPasswordOrService.jsx
@@ -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 (
+
+ { `${this.context.intl.formatMessage({id: 'accounts.or_use'})} ${ labels.join(' / ') }` }
+
+ );
+ }
+ return null;
+ }
+}
+
+AccountsPasswordOrService.propTypes = {
+ oauthServices: PropTypes.object
+};
+
+AccountsPasswordOrService.contextTypes = {
+ intl: intlShape
+};
+
+replaceComponent('AccountsPasswordOrService', AccountsPasswordOrService, [withStyles, styles]);
diff --git a/packages/vulcan-ui-material/lib/components/accounts/AccountsSocialButtons.jsx b/packages/vulcan-ui-material/lib/components/accounts/AccountsSocialButtons.jsx
new file mode 100644
index 000000000..4667b8150
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/accounts/AccountsSocialButtons.jsx
@@ -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(
+
+ {Object.keys(oauthServices).map((id, i) => {
+ return ;
+ })}
+
+ );
+ }
+}
+
+replaceComponent('AccountsSocialButtons', AccountsSocialButtons, [withStyles, styles]);
diff --git a/packages/vulcan-ui-material/lib/components/bonus/LoadMore.jsx b/packages/vulcan-ui-material/lib/components/bonus/LoadMore.jsx
new file mode 100644
index 000000000..934da0c7d
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/bonus/LoadMore.jsx
@@ -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
+ ?
+
+ :
+ loadMore()}>
+
+ ;
+
+ return (
+
+ {
+ showCount &&
+
+
+
+
+ }
+ {
+ isLoadingMore
+
+ ?
+
+
+
+ :
+
+ hasMore
+
+ ?
+
+ infiniteScroll
+
+ ?
+
+ loadMore()}>
+ {loadMoreButton}
+
+
+ :
+
+ loadMoreButton
+ :
+
+ null
+ }
+
+ );
+};
+
+
+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]);
+
diff --git a/packages/vulcan-ui-material/lib/components/bonus/ScrollTrigger.jsx b/packages/vulcan-ui-material/lib/components/bonus/ScrollTrigger.jsx
new file mode 100644
index 000000000..6cbf40aa2
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/bonus/ScrollTrigger.jsx
@@ -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 (
+ {this.element = element;}}>
+ {children}
+
+ );
+ }
+}
+
+
+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;
diff --git a/packages/vulcan-ui-material/lib/components/bonus/SearchInput.jsx b/packages/vulcan-ui-material/lib/components/bonus/SearchInput.jsx
new file mode 100644
index 000000000..bcfb10a27
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/bonus/SearchInput.jsx
@@ -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 = ;
+
+ const clearButton = }
+ 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 (
+
+ 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
+ }}
+ />
+
+ {
+ // KeyboardEventHandler is not valid on the server, where its name is undefined
+ typeof window !== 'undefined' && KeyboardEventHandler.name && !noShortcuts &&
+
+
+ }
+
+
+ );
+ }
+
+}
+
+
+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]);
diff --git a/packages/vulcan-ui-material/lib/components/bonus/TooltipIconButton.jsx b/packages/vulcan-ui-material/lib/components/bonus/TooltipIconButton.jsx
new file mode 100644
index 000000000..45e3ba288
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/bonus/TooltipIconButton.jsx
@@ -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 (
+
+
+ {
+ variant === 'fab'
+
+ ?
+
+
+
+ :
+
+
+ {icon}
+
+ }
+
+
+ );
+
+};
+
+
+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]);
diff --git a/packages/vulcan-ui-material/lib/components/bonus/TooltipIntl.jsx b/packages/vulcan-ui-material/lib/components/bonus/TooltipIntl.jsx
new file mode 100644
index 000000000..5e21e277d
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/bonus/TooltipIntl.jsx
@@ -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 (
+
+
+
+ {
+ variant === 'fab' && !!icon
+
+ ?
+
+
+
+ :
+
+ !!icon
+
+ ?
+
+
+ {iconWithClass}
+
+
+ :
+
+ variant === 'button'
+
+ ?
+
+
+ :
+
+ children
+ }
+
+
+
+ );
+
+};
+
+
+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]);
diff --git a/packages/vulcan-ui-material/lib/components/core/Card.jsx b/packages/vulcan-ui-material/lib/components/core/Card.jsx
new file mode 100644
index 000000000..a311c4841
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/core/Card.jsx
@@ -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 ?
+
:
+ ;
+};
+
+
+const LimitedString = ({ string }) =>
+
+ {string.indexOf(' ') === -1 && string.length > 30 ?
+ {string.substr(0, 30)}… :
+ {string}
+ }
+
;
+
+
+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 ;
+
+ case 'Number':
+ case 'number':
+ case 'SimpleSchema.Integer':
+ return {value.toString()}
;
+
+ case 'Array':
+ return {value.map(
+ (item, index) => - {getFieldValue(item, typeof item, classes)}
)}
;
+
+ case 'Object':
+ case 'object':
+ return (
+
+
+ {_.map(value, (value, key) =>
+
+ {key}
+ {getFieldValue(value, typeof value, classes)}
+
+ )}
+
+
+ );
+
+ 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 }) =>
+
+
+ {label}
+
+
+ {getFieldValue(value, typeName, classes)}
+
+ ;
+
+
+const CardEdit = (props, context) => {
+ const classes = props.classes;
+ const editTitle = context.intl.formatMessage({ id: 'cards.edit' });
+ return (
+
+
+
+
+ }
+ >
+
+
+
+
+ );
+};
+
+
+CardEdit.contextTypes = { intl: intlShape };
+
+
+const CardEditForm = ({ collection, document, closeModal }) =>
+ {
+ 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 (
+
+
+
+ {canUpdate ? : null}
+ {fieldNames.map((fieldName, index) =>
+
+ )}
+
+
+
+ );
+};
+
+
+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]);
diff --git a/packages/vulcan-ui-material/lib/components/core/Datatable.jsx b/packages/vulcan-ui-material/lib/components/core/Datatable.jsx
new file mode 100644
index 000000000..d2561b1b4
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/core/Datatable.jsx
@@ -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 ;
+
+ } 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 (
+
+ {/* DatatableAbove Component part*/}
+ {
+ showSearch &&
+
+
+ }
+ {
+ showNew &&
+
+
+ }
+
+
+
+ );
+ }
+ }
+}
+
+
+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 ;
+ } 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 (
+
+ {
+ (title)?
+
+
+ title
+
+
+ :null
+ }
+
+ {
+ columns &&
+
+
+ {
+ _.sortBy(columns, column => column.order).map(
+ (column, index) =>
+
+ )
+ }
+ {
+ (showEdit || editComponent) &&
+
+
+ }
+
+
+ }
+
+ {
+ results &&
+
+
+ {
+ results.map(
+ (document, index) =>
+ )
+ }
+
+ }
+
+ {
+ footerData &&
+
+
+
+ {
+ _.sortBy(columns, column => column.order).map(
+ (column, index) =>
+
+ {footerData[index]}
+
+ )
+ }
+ {
+ (showEdit || editComponent) &&
+
+
+ }
+
+
+
+ }
+
+
+ {
+ paginate &&
+
+
+ }
+ {
+ !paginate && loadMore &&
+
+
+ }
+
+ );
+};
+
+
+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 ;
+ }
+ } else if (intlNamespace) {
+ formattedLabel = typeof columnName === 'string' ?
+ intl.formatMessage({
+ id: `${intlNamespace}.${columnName}`,
+ defaultMessage: columnName
+ }) :
+ '';
+ } else {
+ formattedLabel = intl.formatMessage({ id: columnName, defaultMessage: columnName });
+ }
+
+ return {formattedLabel};
+};
+
+
+DatatableHeader.contextTypes = {
+ intl: intlShape,
+};
+
+
+replaceComponent('DatatableHeader', DatatableHeader);
+
+
+/*
+
+DatatableSorter Component
+
+*/
+
+const DatatableSorter = ({ name, label, toggleSort, currentSort, sortable }) =>
+
+
+ toggleSort(name)}
+ >
+ {label}
+
+
+ ;
+
+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 (
+ handleRowClick(event, document))}
+ hover
+ >
+
+ {
+ _.sortBy(columns, column => column.order).map(
+ (column, index) =>
+ )
+ }
+
+ {
+ (showEdit || editComponent) &&
+
+
+ {
+ EditComponent &&
+
+
+ }
+ {
+ showEdit &&
+
+
+ }
+
+ }
+
+
+ );
+};
+
+
+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 (
+
+
+
+ );
+};
+
+
+replaceComponent('DatatableCell', DatatableCell);
+
+
+/*
+
+DatatableDefaultCell Component
+
+*/
+const DatatableDefaultCell = ({ column, document }) =>
+
+ {
+ typeof column === 'string'
+ ?
+ getFieldValue(document[column])
+ :
+ getFieldValue(document[column.name])
+ }
+
;
+
+
+replaceComponent('DatatableDefaultCell', DatatableDefaultCell);
diff --git a/packages/vulcan-ui-material/lib/components/core/EditButton.jsx b/packages/vulcan-ui-material/lib/components/core/EditButton.jsx
new file mode 100644
index 000000000..ba1f9ff77
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/core/EditButton.jsx
@@ -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 }
+) => (
+ }
+ color={color}
+ variant={variant}
+ classes={buttonClasses}
+ />
+ }
+ >
+
+
+);
+
+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 (
+
+ );
+};
+
+registerComponent('EditForm', EditForm);
diff --git a/packages/vulcan-ui-material/lib/components/core/Loading.jsx b/packages/vulcan-ui-material/lib/components/core/Loading.jsx
new file mode 100644
index 000000000..dc821a032
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/core/Loading.jsx
@@ -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 ;
+}
+
+replaceComponent('Loading', Loading);
diff --git a/packages/vulcan-ui-material/lib/components/core/ModalTrigger.jsx b/packages/vulcan-ui-material/lib/components/core/ModalTrigger.jsx
new file mode 100644
index 000000000..928256805
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/core/ModalTrigger.jsx
@@ -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'
+ ?
+
+ :
+ {label};
+
+ const childrenComponent = typeof children.type === 'function' ?
+ React.cloneElement(children, { closeModal: this.closeModal }) :
+ children;
+
+ return (
+
+
+ {triggerComponent}
+
+
+
+ );
+ }
+}
+
+
+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]);
diff --git a/packages/vulcan-ui-material/lib/components/core/NewButton.jsx b/packages/vulcan-ui-material/lib/components/core/NewButton.jsx
new file mode 100644
index 000000000..1ce2cbf93
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/core/NewButton.jsx
@@ -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 }) => (
+
+ }
+ color={color}
+ variant={variant}
+ />}
+ >
+
+
+);
+
+
+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);
diff --git a/packages/vulcan-ui-material/lib/components/forms/FormComponentInner.jsx b/packages/vulcan-ui-material/lib/components/forms/FormComponentInner.jsx
new file mode 100644
index 000000000..de8fdf7f2
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/FormComponentInner.jsx
@@ -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 ;
+ } else if (nestedInput){
+ return ;
+ } else {
+ return (
+
+ {instantiateComponent(beforeComponent, properties)}
+
+ {instantiateComponent(afterComponent, properties)}
+
+ );
+ }
+
+ }
+}
+
+
+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]);
diff --git a/packages/vulcan-ui-material/lib/components/forms/FormErrors.jsx b/packages/vulcan-ui-material/lib/components/forms/FormErrors.jsx
new file mode 100644
index 000000000..049d13ce1
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/FormErrors.jsx
@@ -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 = (
+
+ {errors.map((error, index) => (
+ -
+
+
+ ))}
+
+ );
+
+ return (
+
+ {!!errors.length && (
+
+ )}
+
+ );
+};
+
+
+replaceComponent('FormErrors', FormErrors, [withStyles, styles]);
diff --git a/packages/vulcan-ui-material/lib/components/forms/FormGroupNone.jsx b/packages/vulcan-ui-material/lib/components/forms/FormGroupNone.jsx
new file mode 100644
index 000000000..773b11eba
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/FormGroupNone.jsx
@@ -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 (
+
+ );
+ }
+
+
+}
+
+
+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]);
diff --git a/packages/vulcan-ui-material/lib/components/forms/FormGroupWithLine.jsx b/packages/vulcan-ui-material/lib/components/forms/FormGroupWithLine.jsx
new file mode 100644
index 000000000..7ea0c0965
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/FormGroupWithLine.jsx
@@ -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 (
+
+
+
+
+
+
+ {this.props.label}
+
+ {
+ collapsible &&
+
+
+ {
+ this.state.collapsed
+ ?
+
+ :
+
+ }
+
+ }
+
+
+
+ );
+ };
+
+
+ // 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 (
+
+ );
+ }
+}
+
+
+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]);
diff --git a/packages/vulcan-ui-material/lib/components/forms/FormNested.jsx b/packages/vulcan-ui-material/lib/components/forms/FormNested.jsx
new file mode 100644
index 000000000..2b3c7e5db
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/FormNested.jsx
@@ -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 = () => ;
+
+replaceComponent('IconRemove', IconRemove);
+
+const IconAdd = () => ;
+
+replaceComponent('IconAdd', IconAdd);
diff --git a/packages/vulcan-ui-material/lib/components/forms/FormNestedArrayLayout.jsx b/packages/vulcan-ui-material/lib/components/forms/FormNestedArrayLayout.jsx
new file mode 100644
index 000000000..1a7e4e4a0
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/FormNestedArrayLayout.jsx
@@ -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 }) => (
+
+
+ {label}
+
+
{content}
+
+);
+FormNestedArrayLayout.propTypes = {
+ hasErrors: PropTypes.bool,
+ label: PropTypes.node,
+ content: PropTypes.node,
+};
+replaceComponent({
+ name: 'FormNestedArrayLayout',
+ component: FormNestedArrayLayout,
+});
diff --git a/packages/vulcan-ui-material/lib/components/forms/FormNestedDivider.jsx b/packages/vulcan-ui-material/lib/components/forms/FormNestedDivider.jsx
new file mode 100644
index 000000000..4f49615d4
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/FormNestedDivider.jsx
@@ -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 }) => ;
+
+FormNestedDivider.propTypes = {
+ classes: PropTypes.object.isRequired,
+ label: PropTypes.string,
+ addItem: PropTypes.func,
+};
+
+replaceComponent('FormNestedDivider', FormNestedDivider, [withStyles, styles]);
diff --git a/packages/vulcan-ui-material/lib/components/forms/FormNestedFoot.jsx b/packages/vulcan-ui-material/lib/components/forms/FormNestedFoot.jsx
new file mode 100644
index 000000000..c78ab73ad
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/FormNestedFoot.jsx
@@ -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 }) => (
+
+
+
+
+
+);
+
+FormNestedFoot.propTypes = {
+ label: PropTypes.string,
+ addItem: PropTypes.func,
+};
+
+registerComponent('FormNestedFoot', FormNestedFoot);
diff --git a/packages/vulcan-ui-material/lib/components/forms/FormNestedHead.jsx b/packages/vulcan-ui-material/lib/components/forms/FormNestedHead.jsx
new file mode 100644
index 000000000..bd24a88d5
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/FormNestedHead.jsx
@@ -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 }) => ;
+
+FormNestedHead.propTypes = {
+ label: PropTypes.string,
+ addItem: PropTypes.func,
+};
+
+replaceComponent('FormNestedHead', FormNestedHead);
diff --git a/packages/vulcan-ui-material/lib/components/forms/FormSubmit.jsx b/packages/vulcan-ui-material/lib/components/forms/FormSubmit.jsx
new file mode 100644
index 000000000..b8b92a301
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/FormSubmit.jsx
@@ -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 (
+
+
+ {
+ deleteDocument
+ ?
+
+
+
+
+
+ :
+ null
+ }
+
+ {
+ cancelCallback
+ ?
+
+ :
+ null
+ }
+
+ {
+ revertCallback
+ ?
+
+ :
+ null
+ }
+
+
+
+
+ );
+};
+
+
+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]);
diff --git a/packages/vulcan-ui-material/lib/components/forms/base-controls/EndAdornment.jsx b/packages/vulcan-ui-material/lib/components/forms/base-controls/EndAdornment.jsx
new file mode 100644
index 000000000..4f136be08
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/base-controls/EndAdornment.jsx
@@ -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 &&
+ {
+ event.preventDefault();
+ changeValue(null);
+ }}
+ tabIndex="-1"
+ >
+
+ ;
+
+ return (
+
+ {instantiateComponent(addonAfter)}
+ {clearButton}
+
+ );
+};
+
+
+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);
diff --git a/packages/vulcan-ui-material/lib/components/forms/base-controls/MuiCheckboxGroup.jsx b/packages/vulcan-ui-material/lib/components/forms/base-controls/MuiCheckboxGroup.jsx
new file mode 100644
index 000000000..19070819d
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/base-controls/MuiCheckboxGroup.jsx
@@ -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 (
+ 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 (
+
+ {controls}
+
+ );
+ },
+
+ render: function () {
+
+ if (this.props.layout === 'elementOnly') {
+ return (
+ {this.renderElement()}
+ );
+ }
+
+ return (
+
+ {this.renderElement()}
+
+
+ );
+ }
+});
+
+
+export default withStyles(styles)(MuiCheckboxGroup);
diff --git a/packages/vulcan-ui-material/lib/components/forms/base-controls/MuiFormControl.jsx b/packages/vulcan-ui-material/lib/components/forms/base-controls/MuiFormControl.jsx
new file mode 100644
index 000000000..7b426355d
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/base-controls/MuiFormControl.jsx
@@ -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 (
+ *
+ );
+ },
+
+ renderLabel: function () {
+ if (this.props.layout === 'elementOnly' || this.props.hideLabel) {
+ return null;
+ }
+
+ if (this.props.fakeLabel) {
+ return (
+
+ {this.props.label}
+ {this.renderRequiredSymbol()}
+
+ );
+ }
+
+ const shrink = ['date', 'time', 'datetime'].includes(this.props.inputType) ? true : undefined;
+
+ return (
+
+ {this.props.label}
+ {this.renderRequiredSymbol()}
+
+ );
+ },
+
+ render: function () {
+ const { layout, className, children, hasErrors } = this.props;
+
+ if (layout === 'elementOnly') {
+ return {children};
+ }
+
+ return (
+
+ {this.renderLabel()}
+ {children}
+
+ );
+ }
+
+});
+
+
+export default MuiFormControl;
diff --git a/packages/vulcan-ui-material/lib/components/forms/base-controls/MuiFormHelper.jsx b/packages/vulcan-ui-material/lib/components/forms/base-controls/MuiFormHelper.jsx
new file mode 100644
index 000000000..58a5f4e5d
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/base-controls/MuiFormHelper.jsx
@@ -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 &&
+ ;
+
+ return (
+
+
+
+ {
+ hasErrors ? errorMessage : help
+ }
+
+
+ {
+ showCharsRemaining &&
+
+
+ {charsCount} / {max}
+
+ }
+
+
+ );
+};
+
+
+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);
diff --git a/packages/vulcan-ui-material/lib/components/forms/base-controls/MuiInput.jsx b/packages/vulcan-ui-material/lib/components/forms/base-controls/MuiInput.jsx
new file mode 100644
index 000000000..e10e80da1
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/base-controls/MuiInput.jsx
@@ -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 :
+ ;
+ const endAdornment =
+ ;
+
+ let element = this.renderElement(startAdornment, endAdornment);
+
+ if (this.props.layout === 'elementOnly' || this.props.type === 'hidden') {
+ return element;
+ }
+
+ return (
+
+ {element}
+
+
+ );
+ },
+
+ 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 (
+ (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);
diff --git a/packages/vulcan-ui-material/lib/components/forms/base-controls/MuiPicker.jsx b/packages/vulcan-ui-material/lib/components/forms/base-controls/MuiPicker.jsx
new file mode 100644
index 000000000..4b4576c27
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/base-controls/MuiPicker.jsx
@@ -0,0 +1,85 @@
+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 TextField from '@material-ui/core/TextField';
+
+import moment from 'moment';
+//import 'moment-timezone';
+
+const dateFormat = 'YYYY-MM-DD';
+
+export const styles = theme => ({
+ inputRoot: {
+ 'marginTop': '16px',
+ '& .clear-enabled': { opacity: 0 },
+ '&:hover .clear-enabled': { opacity: 0.54 },
+ },
+ inputFocused: {
+ '& .clear-enabled': { opacity: 0.54 }
+ },
+});
+
+//noinspection JSUnusedGlobalSymbols
+const MuiPicker = createReactClass({
+
+ mixins: [ComponentMixin],
+
+ displayName: 'MuiPicker',
+
+ propTypes: {
+ type: PropTypes.oneOf([
+ 'date',
+ 'datetime',
+ 'datetime-local',
+ ]),
+ errors: PropTypes.array,
+ placeholder: PropTypes.string,
+ formatValue: PropTypes.func,
+ hideClear: PropTypes.bool,
+ },
+
+ getDefaultProps: function () {
+ return {
+ type: 'date',
+ };
+ },
+
+ handleChange: function (event) {
+ let value = event.target.value;
+ if (this.props.scrubValue) {
+ value = this.props.scrubValue(value);
+ }
+ this.props.onChange(value);
+ },
+
+ render: function () {
+ const { classes, disabled, autoFocus } = this.props;
+ const value = moment(this.props.value, dateFormat, true).isValid() ? this.props.value : moment(this.props.value).format(dateFormat);
+
+ const options = this.props.options || {};
+
+ return (
+
+ (this.element = c)}
+ {...this.cleanProps(this.props)}
+ id={this.getId()}
+ value={value}
+ autoFocus={options.autoFocus || autoFocus}
+ onChange={this.handleChange}
+ disabled={disabled}
+ placeholder={this.props.placeholder}
+ classes={{ root: classes.inputRoot }}
+ />
+
+
+ );
+ }
+});
+
+
+export default withStyles(styles)(MuiPicker);
diff --git a/packages/vulcan-ui-material/lib/components/forms/base-controls/MuiRadioGroup.jsx b/packages/vulcan-ui-material/lib/components/forms/base-controls/MuiRadioGroup.jsx
new file mode 100644
index 000000000..229b3d7c6
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/base-controls/MuiRadioGroup.jsx
@@ -0,0 +1,158 @@
+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 MuiFormControl from './MuiFormControl';
+import MuiFormHelper from './MuiFormHelper';
+import FormControlLabel from '@material-ui/core/FormControlLabel';
+import Radio from '@material-ui/core/Radio';
+import RadioGroup from '@material-ui/core/RadioGroup';
+import classNames from 'classnames';
+
+
+const styles = theme => ({
+ group: {
+ marginTop: '8px',
+ },
+ inline: {
+ flexDirection: 'row',
+ '& > label': {
+ marginRight: theme.spacing.unit * 5,
+ },
+ },
+ 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%',
+ },
+ },
+ },
+ radio: {
+ width: '32px',
+ height: '32px',
+ marginLeft: '8px',
+ },
+ line: {
+ marginBottom: '12px',
+ },
+});
+
+
+const MuiRadioGroup = createReactClass({
+
+ mixins: [ComponentMixin],
+
+ propTypes: {
+ name: PropTypes.string.isRequired,
+ type: PropTypes.oneOf(['inline', 'stacked']),
+ options: PropTypes.array.isRequired
+ },
+
+
+
+ getDefaultProps: function () {
+ return {
+ type: 'stacked',
+ label: '',
+ help: null,
+ classes: PropTypes.object.isRequired,
+ };
+ },
+
+ changeRadio: function (event) {
+ const value = event.target.value;
+ //this.setValue(value);
+ this.props.onChange(this.props.name, value);
+ },
+
+ validate: function () {
+ if (this.props.onBlur) {
+ this.props.onBlur();
+ }
+ return true;
+ },
+
+ renderElement: function () {
+ const controls = this.props.options.map((radio, key) => {
+ let checked = (this.props.value === radio.value);
+ let disabled = radio.disabled || this.props.disabled;
+
+ return (
+ this['element-' + key] = c}
+ checked={checked}
+ disabled={disabled}
+ />}
+ className={this.props.classes.line}
+ label={radio.label}
+ />
+ );
+ });
+
+ const maxLength = this.props.options.reduce((max, option) =>
+ option.label.length > max ? option.label.length : max, 0);
+
+ let columnClass = maxLength < 18 ? 'threeColumn' : maxLength < 30 ? 'twoColumn' : '';
+ if (this.props.type === 'inline') columnClass = 'inline';
+
+ return (
+
+ {controls}
+
+ );
+ },
+
+ render: function () {
+
+ if (this.props.layout === 'elementOnly') {
+ return (
+ {this.renderElement()}
+ );
+ }
+
+ return (
+
+ {this.renderElement()}
+
+
+ );
+ }
+});
+
+
+export default withStyles(styles)(MuiRadioGroup);
diff --git a/packages/vulcan-ui-material/lib/components/forms/base-controls/MuiSelect.jsx b/packages/vulcan-ui-material/lib/components/forms/base-controls/MuiSelect.jsx
new file mode 100644
index 000000000..8bd7cdfcd
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/base-controls/MuiSelect.jsx
@@ -0,0 +1,210 @@
+import withStyles from '@material-ui/core/styles/withStyles';
+import React from 'react';
+import createReactClass from 'create-react-class';
+import ComponentMixin from './mixins/component';
+import MuiFormControl from './MuiFormControl';
+import MuiFormHelper from './MuiFormHelper';
+import Select from '@material-ui/core/Select';
+import Input from '@material-ui/core/Input';
+import MenuItem from '@material-ui/core/MenuItem';
+import MenuList from '@material-ui/core/MenuList';
+import ListSubheader from '@material-ui/core/ListSubheader';
+import StartAdornment, { hideStartAdornment } from './StartAdornment';
+import EndAdornment from './EndAdornment';
+import _isArray from 'lodash/isArray';
+
+
+export const styles = theme => ({
+
+ inputRoot: {
+ '& .clear-enabled': { opacity: 0 },
+ '&:hover .clear-enabled': { opacity: 0.54 },
+ },
+
+ inputFocused: {
+ '& .clear-enabled': { opacity: 0.54 }
+ },
+
+ menuItem: {
+ paddingTop: 4,
+ paddingBottom: 4,
+ paddingLeft: 9,
+ fontFamily: theme.typography.fontFamily,
+ color: theme.palette.type === 'light' ? 'rgba(0, 0, 0, 0.87)' : theme.palette.common.white,
+ fontSize: theme.typography.pxToRem(16),
+ lineHeight: '1.1875em',
+ },
+
+ input: {
+ paddingLeft: 8,
+ },
+
+});
+
+
+const MuiSelect = createReactClass({
+
+ element: null,
+
+ mixins: [ComponentMixin],
+
+ getInitialState: function () {
+ return {
+ isOpen: false,
+ };
+ },
+
+ handleOpen: function () {
+ // this doesn't work
+ this.setState({
+ isOpen: true,
+ });
+ },
+
+ handleClose: function () {
+ // this doesn't work
+ this.setState({
+ isOpen: false,
+ });
+ },
+
+ handleChange: function (event) {
+ const target = event.target;
+ let value;
+ if (this.props.multiple) {
+ value = [];
+ for (let i = 0; i < target.length; i++) {
+ const option = target.options[i];
+ if (option.selected) {
+ value.push(option.value);
+ }
+ }
+ } else {
+ value = target.value;
+ }
+ this.changeValue(value);
+ },
+
+ changeValue: function (value) {
+ this.props.onChange(value);
+ },
+
+ render: function () {
+ if (this.props.layout === 'elementOnly') {
+ return this.renderElement();
+ }
+
+ return (
+
+ {this.renderElement()}
+
+
+ );
+ },
+
+ renderElement: function () {
+ const renderOption = (item, key) => {
+ //eslint-disable-next-line no-unused-vars
+ const { group, label, ...rest } = item;
+ return this.props.native
+ ?
+
+ :
+ ;
+ };
+
+ const renderGroup = (label, key, nodes) => {
+ return this.props.native
+ ?
+
+ :
+ {label}} key={key}>
+ {nodes}
+ ;
+ };
+
+ const { options, classes } = this.props;
+
+ let groups = options.filter(function (item) {
+ return item.group;
+ }).map(function (item) {
+ return item.group;
+ });
+ // Get the unique items in group.
+ groups = [...new Set(groups)];
+
+ let optionNodes = [];
+
+ if (groups.length === 0) {
+ optionNodes = options.map(function (item, index) {
+ return renderOption(item, index);
+ });
+ } else {
+ // For items without groups.
+ const itemsWithoutGroup = options.filter(function (item) {
+ return !item.group;
+ });
+
+ itemsWithoutGroup.forEach(function (item, index) {
+ optionNodes.push(renderOption(item, 'no-group-' + index));
+ });
+
+ groups.forEach(function (group, groupIndex) {
+
+ const groupItems = options.filter(function (item) {
+ return item.group === group;
+ });
+
+ const groupOptionNodes = groupItems.map(function (item, index) {
+ return renderOption(item, groupIndex + '-' + index);
+ });
+
+ optionNodes.push(renderGroup(group, groupIndex, groupOptionNodes));
+ });
+ }
+
+ let value = this.props.value;
+ if (!this.props.multiple && _isArray(value)) {
+ value = value.length ? value[0] : '';
+ }
+
+ const startAdornment = hideStartAdornment(this.props) ? null :
+ ;
+ const endAdornment =
+ ;
+
+ return (
+
+ );
+ }
+});
+
+
+export default withStyles(styles)(MuiSelect);
diff --git a/packages/vulcan-ui-material/lib/components/forms/base-controls/MuiSuggest.jsx b/packages/vulcan-ui-material/lib/components/forms/base-controls/MuiSuggest.jsx
new file mode 100644
index 000000000..287491b60
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/base-controls/MuiSuggest.jsx
@@ -0,0 +1,437 @@
+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 Input from '@material-ui/core/Input';
+import Autosuggest from 'react-autosuggest';
+import Paper from '@material-ui/core/Paper';
+import MenuItem from '@material-ui/core/MenuItem';
+import match from 'autosuggest-highlight/match';
+import parse from 'autosuggest-highlight/parse';
+import { registerComponent } from 'meteor/vulcan:core';
+import StartAdornment, { hideStartAdornment } from './StartAdornment';
+import EndAdornment from './EndAdornment';
+import MuiFormControl from './MuiFormControl';
+import MuiFormHelper from './MuiFormHelper';
+import _isEqual from 'lodash/isEqual';
+import classNames from 'classnames';
+import IsolatedScroll from 'react-isolated-scroll';
+
+
+const maxSuggestions = 100;
+
+
+/*{
+ container: 'react-autosuggest__container',
+ containerOpen: 'react-autosuggest__container--open',
+ input: 'react-autosuggest__input',
+ inputOpen: 'react-autosuggest__input--open',
+ inputFocused: 'react-autosuggest__input--focused',
+ suggestionsContainer: 'react-autosuggest__suggestions-container',
+ suggestionsContainerOpen: 'react-autosuggest__suggestions-container--open',
+ suggestionsList: 'react-autosuggest__suggestions-list',
+ suggestion: 'react-autosuggest__suggestion',
+ suggestionFirst: 'react-autosuggest__suggestion--first',
+ suggestionHighlighted: 'react-autosuggest__suggestion--highlighted',
+ sectionContainer: 'react-autosuggest__section-container',
+ sectionContainerFirst: 'react-autosuggest__section-container--first',
+ sectionTitle: 'react-autosuggest__section-title'
+}*/
+const styles = theme => ({
+ container: {
+ flexGrow: 1,
+ position: 'relative',
+ },
+ textField: {
+ width: '100%',
+ 'label + div > &': {
+ marginTop: theme.spacing.unit * 2,
+ },
+ },
+ input: {
+ outline: 0,
+ font: 'inherit',
+ color: 'currentColor',
+ width: '100%',
+ border: '0',
+ margin: '0',
+ padding: '7px 0',
+ display: 'block',
+ boxSizing: 'content-box',
+ background: 'none',
+ verticalAlign: 'middle',
+ '&::-webkit-search-decoration, &::-webkit-search-cancel-button, &::after, &:after':
+ { display: 'none' },
+ '&::-webkit-search-results, &::-webkit-search-results-decoration':
+ { display: 'none' },
+ },
+ readOnly: {
+ cursor: 'pointer',
+ },
+ suggestionsContainer: {
+ display: 'none',
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ zIndex: theme.zIndex.modal,
+ marginBottom: theme.spacing.unit * 3,
+ maxHeight: 48 * 8,
+ },
+ suggestionsContainerOpen: {
+ display: 'flex',
+ },
+ scroller: {
+ flexGrow: 1,
+ overflowY: 'auto',
+ },
+ suggestion: {
+ display: 'block',
+ },
+ suggestionIcon: {
+ marginRight: theme.spacing.unit * 2,
+ },
+ selected: {
+ backgroundColor: theme.palette.secondary.light,
+ },
+ suggestionsList: {
+ margin: 0,
+ padding: 0,
+ listStyleType: 'none',
+ },
+ inputRoot: {
+ '& .clear-enabled': { opacity: 0 },
+ '&:hover .clear-enabled': { opacity: 0.54 },
+ },
+ inputFocused: {
+ '& .clear-enabled': { opacity: 0.54 }
+ },
+});
+
+
+const MuiSuggest = createReactClass({
+
+ inputElement: null,
+
+ mixins: [ComponentMixin],
+
+ propTypes: {
+ options: PropTypes.arrayOf(PropTypes.shape({
+ label: PropTypes.string,
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ formatted: PropTypes.node,
+ iconComponent: PropTypes.node,
+ onClick: PropTypes.func,
+ })),
+ classes: PropTypes.object.isRequired,
+ limitToList: PropTypes.bool,
+ disableText: PropTypes.bool,
+ showAllOptions: PropTypes.bool,
+ className: PropTypes.string,
+ autoComplete: PropTypes.string,
+ autoFocus: PropTypes.bool,
+ },
+
+ getDefaultProps: function () {
+ return {
+ autoComplete: 'off',
+ autoFocus: false,
+ };
+ },
+
+ getOptionFormatted: function (option) {
+ return option.formatted || option.label || option.value || '';
+ },
+
+ getOptionLabel: function (option) {
+ return option.label || option.value || '';
+ },
+
+ getInitialState: function () {
+ if (this.props.refFunction) {
+ this.props.refFunction(this);
+ }
+
+ const selectedOption = this.getSelectedOption();
+ return {
+ inputValue: this.getOptionLabel(selectedOption),
+ selectedOption: selectedOption,
+ suggestions: [],
+ };
+ },
+
+ componentWillReceiveProps: function (nextProps) {
+ if (nextProps.value !== this.state.value ||
+ nextProps.options !== this.props.options) {
+ const selectedOption = this.getSelectedOption(nextProps);
+ this.setState({
+ inputValue: this.getOptionLabel(selectedOption),
+ selectedOption: selectedOption,
+ });
+ }
+ },
+
+ shouldComponentUpdate: function (nextProps, nextState) {
+ return !_isEqual(nextState, this.state) ||
+ nextProps.help !== this.props.help ||
+ nextProps.charsCount !== this.props.charsCount ||
+ !_isEqual(nextProps.errors, this.props.errors) ||
+ nextProps.options !== this.props.options;
+ },
+
+ getSelectedOption: function (props) {
+ props = props || this.props;
+ const selectedOption = props.options.find((opt) => opt.value === props.value);
+ return selectedOption || { label: '', value: null };
+ },
+
+ handleFocus: function (event) {
+ if (!this.inputElement) return;
+
+ this.inputElement.select();
+ },
+
+ handleBlur: function (event, { highlightedSuggestion: suggestion }) {
+ if (suggestion) {
+ this.changeValue(suggestion);
+ } else if (this.props.limitToList) {
+ const selectedOption = this.getSelectedOption();
+ this.setState({
+ inputValue: this.getOptionLabel(selectedOption),
+ });
+ }
+ },
+
+ suggestionSelected: function (event, { suggestion }) {
+ this.changeValue(suggestion);
+ },
+
+ changeValue: function (suggestion) {
+ if (!suggestion) {
+ suggestion = { label: '', value: null };
+ }
+ if (suggestion.onClick) {
+ return;
+ }
+ this.setState({
+ selectedOption: suggestion,
+ inputValue: this.getOptionLabel(suggestion),
+ });
+ this.props.onChange(this.props.name, suggestion.value, this.getOptionLabel(suggestion));
+ },
+
+ handleInputChange: function (event) {
+ const value = event.target.value;
+ this.setState({
+ inputValue: value,
+ });
+ },
+
+ handleSuggestionsFetchRequested: function ({ value, reason }) {
+ this.setState({
+ suggestions: this.getSuggestions(value),
+ });
+ },
+
+ handleSuggestionsClearRequested: function () {
+ this.setState({
+ suggestions: [],
+ });
+ },
+
+ shouldRenderSuggestions: function (value) {
+ return true;
+ },
+
+ render: function () {
+ const value = this.props.value;
+
+ const startAdornment = hideStartAdornment(this.props) ? null :
+ ;
+ const endAdornment =
+ ;
+
+ const element = this.renderElement(startAdornment, endAdornment);
+
+ if (this.props.layout === 'elementOnly') {
+ return element;
+ }
+
+ return (
+
+ {element}
+
+
+ );
+ },
+
+ renderElement: function (startAdornment, endAdornment) {
+ const { classes, autoFocus, disableText, showAllOptions } = this.props;
+
+ return (
+
+ );
+ },
+
+ renderInputComponent: function (inputProps) {
+ const { classes, autoFocus, autoComplete, value, ref, startAdornment, endAdornment, disabled, ...rest } = inputProps;
+
+ return (
+ { ref(c); this.inputElement = c; }}
+ type="text"
+ startAdornment={startAdornment}
+ endAdornment={endAdornment}
+ disabled={disabled}
+ inputProps={{
+ ...rest,
+ }}
+ />
+ );
+ },
+
+ renderSuggestion: function (suggestion, { query, isHighlighted }) {
+ const label = this.getOptionFormatted(suggestion);
+ const matches = match(label, query);
+ const parts = parse(label, matches);
+ const isSelected = suggestion.value === this.props.value;
+ const className = isSelected ? this.props.classes.selected : null;
+
+ return (
+
+ );
+ },
+
+ renderSuggestionsContainer: function ({ containerProps, children }) {
+ const { classes } = this.props;
+
+ return (
+
+ );
+ },
+
+ getSuggestionValue: function (suggestion) {
+ return suggestion.value;
+ },
+
+ getSuggestions: function (value) {
+ const inputValue = value.trim().toLowerCase();
+ const inputLength = inputValue.length;
+ let count = 0;
+ const inputMatchesSelection = value === this.getOptionLabel(this.state.selectedOption);
+
+ return (this.props.disableText || this.props.showAllOptions) && inputMatchesSelection ?
+
+ this.props.options.filter(suggestion => {
+ return true;
+ })
+
+ :
+
+ inputLength === 0
+
+ ?
+
+ this.props.options.filter(suggestion => {
+ count += 1;
+ return count <= maxSuggestions;
+ })
+
+ :
+
+ this.props.options.filter(suggestion => {
+ const label = this.getOptionLabel(suggestion);
+ const keep =
+ count < maxSuggestions && label.toLowerCase().slice(0, inputLength) ===
+ inputValue;
+
+ if (keep) {
+ count += 1;
+ }
+
+ return keep;
+ });
+ },
+
+});
+
+
+export default withStyles(styles)(MuiSuggest);
+registerComponent('MuiSuggest', MuiSuggest, [withStyles, styles]);
diff --git a/packages/vulcan-ui-material/lib/components/forms/base-controls/MuiSwitch.jsx b/packages/vulcan-ui-material/lib/components/forms/base-controls/MuiSwitch.jsx
new file mode 100644
index 000000000..1d4c2a5f2
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/base-controls/MuiSwitch.jsx
@@ -0,0 +1,70 @@
+import React from 'react';
+import createReactClass from 'create-react-class';
+import ComponentMixin from './mixins/component';
+import FormControlLabel from '@material-ui/core/FormControlLabel';
+import Switch from '@material-ui/core/Switch';
+import MuiFormControl from './MuiFormControl';
+import MuiFormHelper from './MuiFormHelper';
+
+
+const MuiSwitch = createReactClass({
+
+ mixins: [ComponentMixin],
+
+ getDefaultProps: function () {
+ return {
+ label: '',
+ rowLabel: '',
+ value: false
+ };
+ },
+
+ changeValue: function (event) {
+ const target = event.target;
+ const value = target.checked;
+
+ this.props.onChange(value);
+
+ setTimeout(() => {document.activeElement.blur();});
+ },
+
+ render: function () {
+
+ const element = this.renderElement();
+
+ if (this.props.layout === 'elementOnly') {
+ return element;
+ }
+
+ return (
+
+ {element}
+
+
+ );
+ },
+
+ renderElement: function () {
+ return (
+ this.element = c}
+ {...this.cleanSwitchProps(this.cleanProps(this.props))}
+ id={this.getId()}
+ checked={this.props.value === true}
+ onChange={this.changeValue}
+ disabled={this.props.disabled}
+ />
+ }
+ label={this.props.label}
+ />
+ );
+ },
+
+});
+
+
+export default MuiSwitch;
diff --git a/packages/vulcan-ui-material/lib/components/forms/base-controls/StartAdornment.jsx b/packages/vulcan-ui-material/lib/components/forms/base-controls/StartAdornment.jsx
new file mode 100644
index 000000000..58d6a7434
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/base-controls/StartAdornment.jsx
@@ -0,0 +1,54 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+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 OpenInNewIcon from 'mdi-material-ui/OpenInNew';
+import { styles } from './EndAdornment';
+
+
+export const hideStartAdornment = (props) => {
+ return !props.addonBefore && !props.isUrl;
+};
+
+
+export const fixUrl = (url) => {
+ return url.indexOf('http://') === -1 && url.indexOf('https://') ? 'http://' + url : url;
+};
+
+
+const StartAdornment = (props) => {
+ const { classes, value, type, addonBefore } = props;
+
+ if (hideStartAdornment(props)) return null;
+
+ const urlButton = type === 'url' &&
+
+
+ ;
+
+
+ return (
+
+ {instantiateComponent(addonBefore)}
+ {urlButton}
+
+ );
+};
+
+
+StartAdornment.propTypes = {
+ classes: PropTypes.object.isRequired,
+ value: PropTypes.any,
+ type: PropTypes.string,
+ addonBefore: PropTypes.oneOfType([PropTypes.string, PropTypes.node, PropTypes.func]),
+};
+
+
+export default withStyles(styles)(StartAdornment);
diff --git a/packages/vulcan-ui-material/lib/components/forms/base-controls/mixins/component.jsx b/packages/vulcan-ui-material/lib/components/forms/base-controls/mixins/component.jsx
new file mode 100644
index 000000000..fa74a6414
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/base-controls/mixins/component.jsx
@@ -0,0 +1,140 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { FormattedMessage } from 'meteor/vulcan:i18n';
+import _omit from 'lodash/omit';
+
+
+export default {
+
+ propTypes: {
+ label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
+ hideLabel: PropTypes.bool,
+ layout: PropTypes.string,
+ required: PropTypes.bool,
+ errors: PropTypes.arrayOf(PropTypes.object),
+ },
+
+ getFormControlProperties: function () {
+ return {
+ label: this.props.label,
+ hideLabel: this.props.hideLabel,
+ layout: this.props.layout,
+ required: this.props.required,
+ hasErrors: this.hasErrors(),
+ className: this.props.className,
+ inputType: this.props.inputType,
+ };
+ },
+
+ getFormHelperProperties: function () {
+ return {
+ help: this.props.help,
+ errors: this.props.errors,
+ hasErrors: this.hasErrors(),
+ showCharsRemaining: this.props.showCharsRemaining,
+ charsRemaining: this.props.charsRemaining,
+ charsCount: this.props.charsCount,
+ max: this.props.max,
+ };
+ },
+
+ hashString: function (string) {
+ let hash = 0;
+ for (let i = 0; i < string.length; i++) {
+ hash = (((hash << 5) - hash) + string.charCodeAt(i)) & 0xFFFFFFFF;
+ }
+ return hash;
+ },
+
+ /**
+ * The ID is used as an attribute on the form control, and is used to allow
+ * associating the label element with the form control.
+ *
+ * If we don't explicitly pass an `id` prop, we generate one based on the
+ * `name`, `label` and `itemIndex` (for nested forms) properties.
+ */
+ getId: function () {
+ if (this.props.id) {
+ return this.props.id;
+ }
+ const label = (typeof this.props.label === 'undefined' ? '' : this.props.label);
+ const itemIndex = (typeof this.props.itemIndex=== 'undefined' ? '' : this.props.itemIndex);
+ return [
+ 'frc',
+ this.props.name.split('[').join('_').replace(']', ''),
+ itemIndex,
+ this.hashString(JSON.stringify(label))
+ ].join('-');
+ },
+
+ hasErrors: function () {
+ return !!(this.props.errors && this.props.errors.length);
+ },
+
+ cleanProps: function (props) {
+ const removedFields = [
+ 'beforeComponent',
+ 'afterComponent',
+ 'addonAfter',
+ 'addonBefore',
+ 'help',
+ 'label',
+ 'hideLabel',
+ 'options',
+ 'layout',
+ 'rowLabel',
+ 'validatePristine',
+ 'validateOnSubmit',
+ 'inputClassName',
+ 'optional',
+ 'throwError',
+ 'currentValues',
+ 'addToDeletedValues',
+ 'deletedValues',
+ 'clearFieldErrors',
+ 'formType',
+ 'inputType',
+ 'showCharsRemaining',
+ 'charsCount',
+ 'charsRemaining',
+ 'handleChange',
+ 'document',
+ 'updateCurrentValues',
+ 'classes',
+ 'errors',
+ 'description',
+ 'clearField',
+ 'regEx',
+ 'allowedValues',
+ 'mustComplete',
+ 'renderComponent',
+ 'formInput',
+ 'className',
+ 'formatValue',
+ 'scrubValue',
+ 'custom',
+ 'hideClear',
+ 'inputProperties',
+ 'currentUser',
+ 'nestedSchema',
+ 'parentFieldName',
+ 'itemIndex',
+ 'formComponents',
+ 'autoValue',
+ 'minCount',
+ 'maxCount'
+ ];
+
+ return _omit(props, removedFields);
+ },
+
+ cleanSwitchProps: function (props) {
+ const removedFields = [
+ 'value',
+ 'error',
+ ];
+
+ return _omit(props, removedFields);
+ },
+
+};
diff --git a/packages/vulcan-ui-material/lib/components/forms/controls/Checkbox.jsx b/packages/vulcan-ui-material/lib/components/forms/controls/Checkbox.jsx
new file mode 100644
index 000000000..4f016d1ad
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/controls/Checkbox.jsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import MuiSwitch from '../base-controls/MuiSwitch';
+import { registerComponent } from 'meteor/vulcan:core';
+
+
+const CheckboxComponent = ({ refFunction, ...properties }) =>
+ ;
+
+
+registerComponent('FormComponentCheckbox', CheckboxComponent);
diff --git a/packages/vulcan-ui-material/lib/components/forms/controls/CheckboxGroup.jsx b/packages/vulcan-ui-material/lib/components/forms/controls/CheckboxGroup.jsx
new file mode 100644
index 000000000..226693354
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/controls/CheckboxGroup.jsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import MuiCheckboxGroup from '../base-controls/MuiCheckboxGroup';
+import { registerComponent } from 'meteor/vulcan:core';
+
+
+const CheckboxGroupComponent = ({ refFunction, ...properties }) =>
+ ;
+
+
+registerComponent('FormComponentCheckboxGroup', CheckboxGroupComponent);
diff --git a/packages/vulcan-ui-material/lib/components/forms/controls/CountrySelect.jsx b/packages/vulcan-ui-material/lib/components/forms/controls/CountrySelect.jsx
new file mode 100644
index 000000000..1c4cf14be
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/controls/CountrySelect.jsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import MuiSuggest from '../base-controls/MuiSuggest';
+import { registerComponent } from 'meteor/vulcan:core';
+import { countries } from './countries';
+
+
+const CountrySelect = ({ refFunction, ...properties }) =>
+ ;
+
+
+registerComponent('CountrySelect', CountrySelect);
diff --git a/packages/vulcan-ui-material/lib/components/forms/controls/Date.jsx b/packages/vulcan-ui-material/lib/components/forms/controls/Date.jsx
new file mode 100644
index 000000000..cca995266
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/controls/Date.jsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import MuiPicker from '../base-controls/MuiPicker';
+import { registerComponent } from 'meteor/vulcan:core';
+import withStyles from '@material-ui/core/styles/withStyles';
+
+
+export const styles = theme => ({
+
+ '@global': {
+ 'input[type=date]::-ms-clear, input[type=date]::-ms-reveal': {
+ display: 'none',
+ width: 0,
+ height: 0,
+ },
+ 'input[type=date]::-webkit-search-cancel-button': {
+ display: 'none',
+ '-webkit-appearance': 'none',
+ },
+ 'input[type="date"]::-webkit-clear-button': {
+ display: 'none',
+ '-webkit-appearance': 'none',
+ },
+
+ 'input[type="date"]::-webkit-inner-spin-button,input[type="date"]::-webkit-outer-spin-button': {
+ '-webkit-appearance': 'none',
+ margin: 0,
+ },
+ },
+
+});
+
+const DateComponent = ({ refFunction, classes, ...properties }) =>
+ ;
+
+registerComponent('FormComponentDate', DateComponent, [withStyles, styles]);
diff --git a/packages/vulcan-ui-material/lib/components/forms/controls/DateRdt.jsx b/packages/vulcan-ui-material/lib/components/forms/controls/DateRdt.jsx
new file mode 100644
index 000000000..f012d6717
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/controls/DateRdt.jsx
@@ -0,0 +1,60 @@
+// Deprecated react-datetime version
+
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import DateTimePicker from 'react-datetime';
+import { registerComponent } from 'meteor/vulcan:core';
+
+class DateComponent extends PureComponent {
+
+ constructor(props) {
+ super(props);
+ this.updateDate = this.updateDate.bind(this);
+ }
+
+ // when the datetime picker has mounted, SmartForm will catch the date value (no formsy mixin in this component)
+ // componentDidMount() {
+ // if (this.props.value) {
+ // this.updateDate(this.props.value);
+ // }
+ // }
+
+ updateDate(date) {
+ this.context.updateCurrentValues({[this.props.path]: date});
+ }
+
+ render() {
+
+ const date = this.props.value ? (typeof this.props.value === 'string' ? new Date(this.props.value) : this.props.value) : null;
+
+ return (
+
+
+
+ this.updateDate(newDate)}
+ inputProps={{name: this.props.name}}
+ />
+
+
+ );
+ }
+}
+
+DateComponent.propTypes = {
+ control: PropTypes.any,
+ datatype: PropTypes.any,
+ group: PropTypes.any,
+ label: PropTypes.string,
+ name: PropTypes.string,
+ value: PropTypes.any,
+};
+
+DateComponent.contextTypes = {
+ updateCurrentValues: PropTypes.func,
+};
+
+export default DateComponent;
diff --git a/packages/vulcan-ui-material/lib/components/forms/controls/DateTime.jsx b/packages/vulcan-ui-material/lib/components/forms/controls/DateTime.jsx
new file mode 100644
index 000000000..5fc870a0a
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/controls/DateTime.jsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import MuiInput from '../base-controls/MuiInput';
+import { registerComponent } from 'meteor/vulcan:core';
+import withStyles from '@material-ui/core/styles/withStyles';
+
+
+export const styles = theme => ({
+
+ '@global': {
+ 'input[type=datetime]::-ms-clear, input[type=datetime]::-ms-reveal': {
+ display: 'none',
+ width: 0,
+ height: 0,
+ },
+ 'input[type=datetime]::-webkit-search-cancel-button': {
+ display: 'none',
+ '-webkit-appearance': 'none',
+ },
+ 'input[type="datetime"]::-webkit-clear-button': {
+ display: 'none',
+ '-webkit-appearance': 'none',
+ },
+
+ 'input[type="datetime"]::-webkit-inner-spin-button,input[type="datetime"]::-webkit-outer-spin-button': {
+ '-webkit-appearance': 'none',
+ margin: 0,
+ },
+ },
+
+});
+
+
+const DateTimeComponent = ({ refFunction, classes, ...properties }) =>
+ ;
+
+
+registerComponent('FormComponentDateTime', DateTimeComponent, [withStyles, styles]);
diff --git a/packages/vulcan-ui-material/lib/components/forms/controls/DateTimeRdt.jsx b/packages/vulcan-ui-material/lib/components/forms/controls/DateTimeRdt.jsx
new file mode 100644
index 000000000..5ce0c84d0
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/controls/DateTimeRdt.jsx
@@ -0,0 +1,61 @@
+// Deprecated react-datetime version
+
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import DateTimePicker from 'react-datetime';
+import { registerComponent } from 'meteor/vulcan:core';
+
+
+class DateTimeRdt extends PureComponent {
+
+ constructor(props) {
+ super(props);
+ this.updateDate = this.updateDate.bind(this);
+ }
+
+ // when the datetime picker has mounted, SmartForm will catch the date value (no formsy mixin in this component)
+ componentDidMount() {
+ if (this.props.value) {
+ this.updateDate(this.props.value);
+ }
+ }
+
+ updateDate(date) {
+ this.context.updateCurrentValues({[this.props.name]: date});
+ }
+
+ render() {
+
+ const date = this.props.value ? (typeof this.props.value === 'string' ? new Date(this.props.value) : this.props.value) : null;
+
+ return (
+
+
+
+ this.updateDate(newDate._d)}
+ format={'x'}
+ inputProps={{name: this.props.name}}
+ />
+
+
+ );
+ }
+}
+
+DateTimeRdt.propTypes = {
+ control: PropTypes.any,
+ datatype: PropTypes.any,
+ group: PropTypes.any,
+ label: PropTypes.string,
+ name: PropTypes.string,
+ value: PropTypes.any,
+};
+
+DateTimeRdt.contextTypes = {
+ updateCurrentValues: PropTypes.func,
+};
+
+export default DateTimeRdt;
diff --git a/packages/vulcan-ui-material/lib/components/forms/controls/Default.jsx b/packages/vulcan-ui-material/lib/components/forms/controls/Default.jsx
new file mode 100644
index 000000000..e35f64abd
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/controls/Default.jsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import MuiInput from '../base-controls/MuiInput';
+import { registerComponent } from 'meteor/vulcan:core';
+
+
+const Default = ({ refFunction, ...properties }) =>
+ ;
+
+
+registerComponent('FormComponentDefault', Default);
diff --git a/packages/vulcan-ui-material/lib/components/forms/controls/Email.jsx b/packages/vulcan-ui-material/lib/components/forms/controls/Email.jsx
new file mode 100644
index 000000000..f4688361f
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/controls/Email.jsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import MuiInput from '../base-controls/MuiInput';
+import { registerComponent } from 'meteor/vulcan:core';
+
+
+const EmailComponent = ({ refFunction, ...properties }) =>
+ ;
+
+
+registerComponent('FormComponentEmail', EmailComponent);
diff --git a/packages/vulcan-ui-material/lib/components/forms/controls/Number.jsx b/packages/vulcan-ui-material/lib/components/forms/controls/Number.jsx
new file mode 100644
index 000000000..03136d0c3
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/controls/Number.jsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import MuiInput from '../base-controls/MuiInput';
+import { registerComponent } from 'meteor/vulcan:core';
+
+
+const NumberComponent = ({ refFunction, ...properties }) =>
+ ;
+
+
+registerComponent('FormComponentNumber', NumberComponent);
diff --git a/packages/vulcan-ui-material/lib/components/forms/controls/PostalCode.jsx b/packages/vulcan-ui-material/lib/components/forms/controls/PostalCode.jsx
new file mode 100644
index 000000000..810433e6c
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/controls/PostalCode.jsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import MuiInput from '../base-controls/MuiInput';
+import { registerComponent } from 'meteor/vulcan:core';
+import { getCountryInfo } from './RegionSelect';
+
+
+const PostalCode = ({ classes, refFunction, ...properties }) => {
+ const currentCountryInfo = getCountryInfo(properties);
+ const postalLabel = currentCountryInfo ? currentCountryInfo.postalLabel : 'Postal code';
+
+ return ;
+};
+
+
+registerComponent('PostalCode', PostalCode);
diff --git a/packages/vulcan-ui-material/lib/components/forms/controls/RadioGroup.jsx b/packages/vulcan-ui-material/lib/components/forms/controls/RadioGroup.jsx
new file mode 100644
index 000000000..63d0052e9
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/controls/RadioGroup.jsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import MuiRadioGroup from '../base-controls/MuiRadioGroup';
+import { registerComponent } from 'meteor/vulcan:core';
+
+
+const RadioGroupComponent = ({ refFunction, ...properties }) =>
+ ;
+
+
+registerComponent('FormComponentRadioGroup', RadioGroupComponent);
diff --git a/packages/vulcan-ui-material/lib/components/forms/controls/RegionSelect.jsx b/packages/vulcan-ui-material/lib/components/forms/controls/RegionSelect.jsx
new file mode 100644
index 000000000..a790bb351
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/controls/RegionSelect.jsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import MuiSuggest from '../base-controls/MuiSuggest';
+import MuiInput from '../base-controls/MuiInput';
+import { registerComponent } from 'meteor/vulcan:core';
+import { countryInfo } from './countries';
+import _get from 'lodash/get';
+
+
+export const getCountryInfo = function (formComponentProps) {
+ const addressPath = formComponentProps.path;
+ const countryParts = addressPath.split('.');
+ countryParts[countryParts.length-1] = 'country';
+ const country = _get(formComponentProps.document, countryParts);
+ return country && countryInfo[country];
+};
+
+
+const RegionSelect = ({ classes, refFunction, ...properties }) => {
+ const currentCountryInfo = getCountryInfo(properties);
+ const options = currentCountryInfo ? currentCountryInfo.regions : null;
+ const regionLabel = currentCountryInfo ? currentCountryInfo.regionLabel : 'Region';
+
+ if (options) {
+ return ;
+ } else {
+ return ;
+ }
+};
+
+
+registerComponent('RegionSelect', RegionSelect);
diff --git a/packages/vulcan-ui-material/lib/components/forms/controls/Select.jsx b/packages/vulcan-ui-material/lib/components/forms/controls/Select.jsx
new file mode 100644
index 000000000..7ff92e343
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/controls/Select.jsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import MuiSelect from '../base-controls/MuiSelect';
+import { registerComponent } from 'meteor/vulcan:core';
+
+
+const SelectComponent = ({ refFunction, ...properties }) => {
+ const noneOption = { label: '', value: '' };
+ properties.options = [noneOption, ...properties.options];
+
+ return ;
+};
+
+
+registerComponent('FormComponentSelect', SelectComponent);
diff --git a/packages/vulcan-ui-material/lib/components/forms/controls/SelectMultiple.jsx b/packages/vulcan-ui-material/lib/components/forms/controls/SelectMultiple.jsx
new file mode 100644
index 000000000..5b3f552ce
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/controls/SelectMultiple.jsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import MuiSelect from '../base-controls/MuiSelect';
+import { registerComponent } from 'meteor/vulcan:core';
+
+
+const SelectMultiple = ({ refFunction, ...properties }) => {
+ properties.multiple = true;
+
+ return ;
+};
+
+
+registerComponent('FormComponentSelectMultiple', SelectMultiple);
diff --git a/packages/vulcan-ui-material/lib/components/forms/controls/Textarea.jsx b/packages/vulcan-ui-material/lib/components/forms/controls/Textarea.jsx
new file mode 100644
index 000000000..988960741
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/controls/Textarea.jsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import MuiInput from '../base-controls/MuiInput';
+import { registerComponent } from 'meteor/vulcan:core';
+
+
+const TextareaComponent = ({ refFunction, ...properties }) =>
+ ;
+
+
+registerComponent('FormComponentTextarea', TextareaComponent);
diff --git a/packages/vulcan-ui-material/lib/components/forms/controls/Time.jsx b/packages/vulcan-ui-material/lib/components/forms/controls/Time.jsx
new file mode 100644
index 000000000..e52bb3cbb
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/controls/Time.jsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import MuiInput from '../base-controls/MuiInput';
+import { registerComponent } from 'meteor/vulcan:core';
+import withStyles from '@material-ui/core/styles/withStyles';
+
+
+export const styles = theme => ({
+
+ '@global': {
+ 'input[type=time]::-ms-clear, input[type=time]::-ms-reveal': {
+ display: 'none',
+ width: 0,
+ height: 0,
+ },
+ 'input[type=time]::-webkit-search-cancel-button': {
+ display: 'none',
+ '-webkit-appearance': 'none',
+ },
+ 'input[type="time"]::-webkit-clear-button': {
+ display: 'none',
+ '-webkit-appearance': 'none',
+ },
+
+ 'input[type="time"]::-webkit-inner-spin-button,input[type="time"]::-webkit-outer-spin-button': {
+ '-webkit-appearance': 'none',
+ margin: 0,
+ },
+ },
+
+});
+
+
+const TimeComponent = ({ refFunction, classes, ...properties }) =>
+ ;
+
+
+registerComponent('FormComponentTime', TimeComponent, [withStyles, styles]);
diff --git a/packages/vulcan-ui-material/lib/components/forms/controls/TimeRdt.jsx b/packages/vulcan-ui-material/lib/components/forms/controls/TimeRdt.jsx
new file mode 100644
index 000000000..cf8334241
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/controls/TimeRdt.jsx
@@ -0,0 +1,73 @@
+// Deprecated react-datetime version
+
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import DateTimePicker from 'react-datetime';
+import { registerComponent } from 'meteor/vulcan:core';
+
+class TimeRdt extends PureComponent {
+
+ constructor(props) {
+ super(props);
+ this.updateDate = this.updateDate.bind(this);
+ }
+
+ // when the datetime picker has mounted, SmartForm will catch the date value (no formsy mixin in this component)
+ // componentDidMount() {
+ // if (this.props.value) {
+ // this.context.updateCurrentValues({[this.props.path]: this.props.value});
+ // }
+ // }
+
+ updateDate(mDate) {
+ // if this is a properly formatted moment date, update time
+ if (typeof mDate === 'object') {
+ this.context.updateCurrentValues({[this.props.path]: mDate.format('HH:mm')});
+ }
+ }
+
+ render() {
+
+ const date = new Date();
+
+ // transform time string into date object to work inside datetimepicker
+ const time = this.props.value;
+ if (time) {
+ date.setHours(parseInt(time.substr(0,2)), parseInt(time.substr(3,5)));
+ } else {
+ date.setHours(0,0);
+ }
+
+ return (
+
+
+
+ this.updateDate(newDate)}
+ inputProps={{name: this.props.name}}
+ />
+
+
+ );
+ }
+}
+
+TimeRdt.propTypes = {
+ control: PropTypes.any,
+ datatype: PropTypes.any,
+ group: PropTypes.any,
+ label: PropTypes.string,
+ name: PropTypes.string,
+ value: PropTypes.any,
+};
+
+TimeRdt.contextTypes = {
+ updateCurrentValues: PropTypes.func,
+};
+
+export default TimeRdt;
diff --git a/packages/vulcan-ui-material/lib/components/forms/controls/Url.jsx b/packages/vulcan-ui-material/lib/components/forms/controls/Url.jsx
new file mode 100644
index 000000000..d5d9ebf13
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/controls/Url.jsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import MuiInput from '../base-controls/MuiInput';
+import { registerComponent } from 'meteor/vulcan:core';
+
+
+const UrlComponent = ({ refFunction, ...properties }) =>
+ ;
+
+
+registerComponent('FormComponentUrl', UrlComponent);
diff --git a/packages/vulcan-ui-material/lib/components/forms/controls/countries.js b/packages/vulcan-ui-material/lib/components/forms/controls/countries.js
new file mode 100644
index 000000000..a2d84b1e7
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/forms/controls/countries.js
@@ -0,0 +1,402 @@
+export const countries = [
+ { value: 'AF', label: 'Afghanistan' },
+ { value: 'AX', label: 'Åland Islands' },
+ { value: 'AL', label: 'Albania' },
+ { value: 'DZ', label: 'Algeria' },
+ { value: 'AS', label: 'American Samoa' },
+ { value: 'AD', label: 'Andorra' },
+ { value: 'AO', label: 'Angola' },
+ { value: 'AI', label: 'Anguilla' },
+ { value: 'AQ', label: 'Antarctica' },
+ { value: 'AG', label: 'Antigua and Barbuda' },
+ { value: 'AR', label: 'Argentina' },
+ { value: 'AM', label: 'Armenia' },
+ { value: 'AW', label: 'Aruba' },
+ { value: 'AU', label: 'Australia' },
+ { value: 'AT', label: 'Austria' },
+ { value: 'AZ', label: 'Azerbaijan' },
+ { value: 'BS', label: 'Bahamas' },
+ { value: 'BH', label: 'Bahrain' },
+ { value: 'BD', label: 'Bangladesh' },
+ { value: 'BB', label: 'Barbados' },
+ { value: 'BY', label: 'Belarus' },
+ { value: 'BE', label: 'Belgium' },
+ { value: 'BZ', label: 'Belize' },
+ { value: 'BJ', label: 'Benin' },
+ { value: 'BM', label: 'Bermuda' },
+ { value: 'BT', label: 'Bhutan' },
+ { value: 'BO', label: 'Bolivia' },
+ { value: 'BQ', label: 'Bonaire, Sint Eustatius, Saba' },
+ { value: 'BA', label: 'Bosnia and Herzegovina' },
+ { value: 'BW', label: 'Botswana' },
+ { value: 'BV', label: 'Bouvet Island' },
+ { value: 'BR', label: 'Brazil' },
+ { value: 'IO', label: 'British Indian Ocean Territory' },
+ { value: 'BN', label: 'Brunei Darussalam' },
+ { value: 'BG', label: 'Bulgaria' },
+ { value: 'BF', label: 'Burkina Faso' },
+ { value: 'BI', label: 'Burundi' },
+ { value: 'CV', label: 'Cabo Verde' },
+ { value: 'KH', label: 'Cambodia' },
+ { value: 'CM', label: 'Cameroon' },
+ { value: 'CA', label: 'Canada' },
+ { value: 'KY', label: 'Cayman Islands' },
+ { value: 'CF', label: 'Central African Republic' },
+ { value: 'TD', label: 'Chad' },
+ { value: 'CL', label: 'Chile' },
+ { value: 'CN', label: 'China' },
+ { value: 'CX', label: 'Christmas Island' },
+ { value: 'CC', label: 'Cocos (Keeling) Islands' },
+ { value: 'CO', label: 'Colombia' },
+ { value: 'KM', label: 'Comoros' },
+ { value: 'CG', label: 'Congo' },
+ { value: 'CD', label: 'Congo (Democratic Republic of the)' },
+ { value: 'CK', label: 'Cook Islands' },
+ { value: 'CR', label: 'Costa Rica' },
+ { value: 'CI', label: 'Côte d’Ivoire' },
+ { value: 'HR', label: 'Croatia' },
+ { value: 'CU', label: 'Cuba' },
+ { value: 'CW', label: 'Curaçao' },
+ { value: 'CY', label: 'Cyprus' },
+ { value: 'CZ', label: 'Czechia' },
+ { value: 'DK', label: 'Denmark' },
+ { value: 'DJ', label: 'Djibouti' },
+ { value: 'DM', label: 'Dominica' },
+ { value: 'DO', label: 'Dominican Republic' },
+ { value: 'EC', label: 'Ecuador' },
+ { value: 'EG', label: 'Egypt' },
+ { value: 'SV', label: 'El Salvador' },
+ { value: 'GQ', label: 'Equatorial Guinea' },
+ { value: 'ER', label: 'Eritrea' },
+ { value: 'EE', label: 'Estonia' },
+ { value: 'ET', label: 'Ethiopia' },
+ { value: 'FK', label: 'Falkland Islands' },
+ { value: 'FO', label: 'Faroe Islands' },
+ { value: 'FJ', label: 'Fiji' },
+ { value: 'FI', label: 'Finland' },
+ { value: 'FR', label: 'France' },
+ { value: 'GF', label: 'French Guiana' },
+ { value: 'PF', label: 'French Polynesia' },
+ { value: 'TF', label: 'French Southern Territories' },
+ { value: 'GA', label: 'Gabon' },
+ { value: 'GM', label: 'Gambia' },
+ { value: 'GE', label: 'Georgia' },
+ { value: 'DE', label: 'Germany' },
+ { value: 'GH', label: 'Ghana' },
+ { value: 'GI', label: 'Gibraltar' },
+ { value: 'GR', label: 'Greece' },
+ { value: 'GL', label: 'Greenland' },
+ { value: 'GD', label: 'Grenada' },
+ { value: 'GP', label: 'Guadeloupe' },
+ { value: 'GU', label: 'Guam' },
+ { value: 'GT', label: 'Guatemala' },
+ { value: 'GG', label: 'Guernsey' },
+ { value: 'GN', label: 'Guinea' },
+ { value: 'GW', label: 'Guinea-Bissau' },
+ { value: 'GY', label: 'Guyana' },
+ { value: 'HT', label: 'Haiti' },
+ { value: 'HM', label: 'Heard Island, Mcdonald Islands' },
+ { value: 'VA', label: 'Vatican City State' },
+ { value: 'HN', label: 'Honduras' },
+ { value: 'HK', label: 'Hong Kong' },
+ { value: 'HU', label: 'Hungary' },
+ { value: 'IS', label: 'Iceland' },
+ { value: 'IN', label: 'India' },
+ { value: 'ID', label: 'Indonesia' },
+ { value: 'IR', label: 'Iran' },
+ { value: 'IQ', label: 'Iraq' },
+ { value: 'IE', label: 'Ireland' },
+ { value: 'IM', label: 'Isle of Man' },
+ { value: 'IL', label: 'Israel' },
+ { value: 'IT', label: 'Italy' },
+ { value: 'JM', label: 'Jamaica' },
+ { value: 'JP', label: 'Japan' },
+ { value: 'JE', label: 'Jersey' },
+ { value: 'JO', label: 'Jordan' },
+ { value: 'KZ', label: 'Kazakhstan' },
+ { value: 'KE', label: 'Kenya' },
+ { value: 'KI', label: 'Kiribati' },
+ { value: 'KW', label: 'Kuwait' },
+ { value: 'KG', label: 'Kyrgyzstan' },
+ { value: 'LA', label: 'Lao' },
+ { value: 'LV', label: 'Latvia' },
+ { value: 'LB', label: 'Lebanon' },
+ { value: 'LS', label: 'Lesotho' },
+ { value: 'LR', label: 'Liberia' },
+ { value: 'LY', label: 'Libya' },
+ { value: 'LI', label: 'Liechtenstein' },
+ { value: 'LT', label: 'Lithuania' },
+ { value: 'LU', label: 'Luxembourg' },
+ { value: 'MO', label: 'Macao' },
+ { value: 'MK', label: 'Macedonia' },
+ { value: 'MG', label: 'Madagascar' },
+ { value: 'MW', label: 'Malawi' },
+ { value: 'MY', label: 'Malaysia' },
+ { value: 'MV', label: 'Maldives' },
+ { value: 'ML', label: 'Mali' },
+ { value: 'MT', label: 'Malta' },
+ { value: 'MH', label: 'Marshall Islands' },
+ { value: 'MQ', label: 'Martinique' },
+ { value: 'MR', label: 'Mauritania' },
+ { value: 'MU', label: 'Mauritius' },
+ { value: 'YT', label: 'Mayotte' },
+ { value: 'MX', label: 'Mexico' },
+ { value: 'FM', label: 'Micronesia' },
+ { value: 'MD', label: 'Moldova' },
+ { value: 'MC', label: 'Monaco' },
+ { value: 'MN', label: 'Mongolia' },
+ { value: 'ME', label: 'Montenegro' },
+ { value: 'MS', label: 'Montserrat' },
+ { value: 'MA', label: 'Morocco' },
+ { value: 'MZ', label: 'Mozambique' },
+ { value: 'MM', label: 'Myanmar' },
+ { value: 'NA', label: 'Namibia' },
+ { value: 'NR', label: 'Nauru' },
+ { value: 'NP', label: 'Nepal' },
+ { value: 'NL', label: 'Netherlands' },
+ { value: 'NC', label: 'New Caledonia' },
+ { value: 'NZ', label: 'New Zealand' },
+ { value: 'NI', label: 'Nicaragua' },
+ { value: 'NE', label: 'Niger' },
+ { value: 'NG', label: 'Nigeria' },
+ { value: 'NU', label: 'Niue' },
+ { value: 'NF', label: 'Norfolk Island' },
+ { value: 'KP', label: 'North Korea' },
+ { value: 'MP', label: 'Northern Mariana Islands' },
+ { value: 'NO', label: 'Norway' },
+ { value: 'OM', label: 'Oman' },
+ { value: 'PK', label: 'Pakistan' },
+ { value: 'PW', label: 'Palau' },
+ { value: 'PS', label: 'Palestine' },
+ { value: 'PA', label: 'Panama' },
+ { value: 'PG', label: 'Papua New Guinea' },
+ { value: 'PY', label: 'Paraguay' },
+ { value: 'PE', label: 'Peru' },
+ { value: 'PH', label: 'Philippines' },
+ { value: 'PN', label: 'Pitcairn' },
+ { value: 'PL', label: 'Poland' },
+ { value: 'PT', label: 'Portugal' },
+ { value: 'PR', label: 'Puerto Rico' },
+ { value: 'QA', label: 'Qatar' },
+ { value: 'RE', label: 'Réunion' },
+ { value: 'RO', label: 'Romania' },
+ { value: 'RU', label: 'Russian Federation' },
+ { value: 'RW', label: 'Rwanda' },
+ { value: 'BL', label: 'Saint Barthélemy' },
+ { value: 'SH', label: 'Saint Helena, Ascension, Tristan Da Cunha' },
+ { value: 'KN', label: 'Saint Kitts and Nevis' },
+ { value: 'LC', label: 'Saint Lucia' },
+ { value: 'MF', label: 'Saint Martin (French Portion)' },
+ { value: 'PM', label: 'Saint Pierre and Miquelon' },
+ { value: 'VC', label: 'Saint Vincent and the Grenadines' },
+ { value: 'WS', label: 'Samoa' },
+ { value: 'SM', label: 'San Marino' },
+ { value: 'ST', label: 'Sao Tome and Principe' },
+ { value: 'SA', label: 'Saudi Arabia' },
+ { value: 'SN', label: 'Senegal' },
+ { value: 'RS', label: 'Serbia' },
+ { value: 'SC', label: 'Seychelles' },
+ { value: 'SL', label: 'Sierra Leone' },
+ { value: 'SG', label: 'Singapore' },
+ { value: 'SX', label: 'Sint Maarten (Dutch part)' },
+ { value: 'SK', label: 'Slovakia' },
+ { value: 'SI', label: 'Slovenia' },
+ { value: 'SB', label: 'Solomon Islands' },
+ { value: 'SO', label: 'Somalia' },
+ { value: 'ZA', label: 'South Africa' },
+ { value: 'GS', label: 'South Georgia, South Sandwich Islands' },
+ { value: 'KR', label: 'South Korea' },
+ { value: 'SS', label: 'South Sudan' },
+ { value: 'ES', label: 'Spain' },
+ { value: 'LK', label: 'Sri Lanka' },
+ { value: 'SD', label: 'Sudan' },
+ { value: 'SR', label: 'Suriname' },
+ { value: 'SJ', label: 'Svalbard and Jan Mayen' },
+ { value: 'SZ', label: 'Swaziland' },
+ { value: 'SE', label: 'Sweden' },
+ { value: 'CH', label: 'Switzerland' },
+ { value: 'SY', label: 'Syria' },
+ { value: 'TW', label: 'Taiwan' },
+ { value: 'TJ', label: 'Tajikistan' },
+ { value: 'TZ', label: 'Tanzania' },
+ { value: 'TH', label: 'Thailand' },
+ { value: 'TL', label: 'Timor-Leste' },
+ { value: 'TG', label: 'Togo' },
+ { value: 'TK', label: 'Tokelau' },
+ { value: 'TO', label: 'Tonga' },
+ { value: 'TT', label: 'Trinidad and Tobago' },
+ { value: 'TN', label: 'Tunisia' },
+ { value: 'TR', label: 'Turkey' },
+ { value: 'TM', label: 'Turkmenistan' },
+ { value: 'TC', label: 'Turks and Caicos Islands' },
+ { value: 'TV', label: 'Tuvalu' },
+ { value: 'UG', label: 'Uganda' },
+ { value: 'UA', label: 'Ukraine' },
+ { value: 'AE', label: 'United Arab Emirates' },
+ { value: 'GB', label: 'United Kingdom' },
+ { value: 'US', label: 'United States' },
+ { value: 'UM', label: 'United States Minor Outlying Islands' },
+ { value: 'UY', label: 'Uruguay' },
+ { value: 'UZ', label: 'Uzbekistan' },
+ { value: 'VU', label: 'Vanuatu' },
+ { value: 'VE', label: 'Venezuela' },
+ { value: 'VN', label: 'Viet Nam' },
+ { value: 'VG', label: 'Virgin Islands, British' },
+ { value: 'VI', label: 'Virgin Islands, U.S.' },
+ { value: 'WF', label: 'Wallis and Futuna' },
+ { value: 'EH', label: 'Western Sahara' },
+ { value: 'YE', label: 'Yemen' },
+ { value: 'ZM', label: 'Zambia' },
+ { value: 'ZW', label: 'Zimbabwe' },
+];
+
+
+export const countryInfo = {
+ US: {
+ regionLabel: 'State',
+ postalLabel: 'Zip code',
+ regions: [
+ { value: 'AL', label: 'Alabama' },
+ { value: 'AK', label: 'Alaska' },
+ { value: 'AZ', label: 'Arizona' },
+ { value: 'AR', label: 'Arkansas' },
+ { value: 'CA', label: 'California' },
+ { value: 'CO', label: 'Colorado' },
+ { value: 'CT', label: 'Connecticut' },
+ { value: 'DE', label: 'Delaware' },
+ { value: 'FL', label: 'Florida' },
+ { value: 'GA', label: 'Georgia' },
+ { value: 'HI', label: 'Hawaii' },
+ { value: 'ID', label: 'Idaho' },
+ { value: 'IL', label: 'Illinois' },
+ { value: 'IN', label: 'Indiana' },
+ { value: 'IA', label: 'Iowa' },
+ { value: 'KS', label: 'Kansas' },
+ { value: 'KY', label: 'Kentucky' },
+ { value: 'LA', label: 'Louisiana' },
+ { value: 'ME', label: 'Maine' },
+ { value: 'MD', label: 'Maryland' },
+ { value: 'MA', label: 'Massachusetts' },
+ { value: 'MI', label: 'Michigan' },
+ { value: 'MN', label: 'Minnesota' },
+ { value: 'MS', label: 'Mississippi' },
+ { value: 'MO', label: 'Missouri' },
+ { value: 'MT', label: 'Montana' },
+ { value: 'NE', label: 'Nebraska' },
+ { value: 'NV', label: 'Nevada' },
+ { value: 'NH', label: 'New Hampshire' },
+ { value: 'NJ', label: 'New Jersey' },
+ { value: 'NM', label: 'New Mexico' },
+ { value: 'NY', label: 'New York' },
+ { value: 'NC', label: 'North Carolina' },
+ { value: 'ND', label: 'North Dakota' },
+ { value: 'OH', label: 'Ohio' },
+ { value: 'OK', label: 'Oklahoma' },
+ { value: 'OR', label: 'Oregon' },
+ { value: 'PA', label: 'Pennsylvania' },
+ { value: 'RI', label: 'Rhode Island' },
+ { value: 'SC', label: 'South Carolina' },
+ { value: 'SD', label: 'South Dakota' },
+ { value: 'TN', label: 'Tennessee' },
+ { value: 'TX', label: 'Texas' },
+ { value: 'UT', label: 'Utah' },
+ { value: 'VT', label: 'Vermont' },
+ { value: 'VA', label: 'Virginia' },
+ { value: 'WA', label: 'Washington' },
+ { value: 'WV', label: 'West Virginia' },
+ { value: 'WI', label: 'Wisconsin' },
+ { value: 'WY', label: 'Wyoming' },
+ ]
+ },
+ CA: {
+ regionLabel: 'Province',
+ postalLabel: 'Postal code',
+ regions: [
+ { value: 'AB', label: 'Alberta' },
+ { value: 'BC', label: 'British Columbia' },
+ { value: 'MB', label: 'Manitoba' },
+ { value: 'NB', label: 'New Brunswick' },
+ { value: 'NL', label: 'Newfoundland and Labrador' },
+ { value: 'NS', label: 'Nova Scotia' },
+ { value: 'NT', label: 'Northwest Territories' },
+ { value: 'NU', label: 'Nunavut' },
+ { value: 'ON', label: 'Ontario' },
+ { value: 'PE', label: 'Prince Edward Island' },
+ { value: 'QC', label: 'Quebec' },
+ { value: 'SK', label: 'Saskatchewan' },
+ { value: 'YT', label: 'Yukon' },
+ ],
+ },
+ AU: {
+ regionLabel: 'State',
+ postalLabel: 'Postcode',
+ regions: [
+ { value: 'ACT', label: 'Australian Capital Territory' },
+ { value: 'NSW', label: 'New South Wales' },
+ { value: 'NT', label: 'Northern Territory' },
+ { value: 'QLD', label: 'Queensland' },
+ { value: 'SA', label: 'South Australia' },
+ { value: 'TAS', label: 'Tasmania' },
+ { value: 'VIC', label: 'Victoria' },
+ { value: 'WA', label: 'Western Australia' },
+ ],
+ },
+ UK: {
+ regionLabel: 'County',
+ postalLabel: 'Postcode',
+ },
+};
+
+
+export const getCountryLabel = (countryValue) => {
+ const country = countries.find(country => country.value === countryValue);
+ return country ? country.label : '';
+};
+
+
+export const getCountryContinent = (countryValue) => {
+ const country = countries.find(country => country.value === countryValue);
+ return country ? country.continent : '';
+};
+
+
+export const getRegionLabel = (countryValue, regionValue) => {
+ if (!countryInfo[countryValue] || !countryInfo[countryValue].regions) {
+ return regionValue;
+ }
+
+ const regions = countryInfo[countryValue].regions;
+
+ let region = regions.find(nextRegion => nextRegion.value === regionValue);
+
+ if (region) {
+ return region.label;
+ } else {
+ return regionValue;
+ }
+};
+
+
+// Given a region value or label, returns the region value (QC or Quebec => QC)
+// or false if the regionValue is invalid
+export const validateRegion = (countryValue, regionValue) => {
+ if (!countryInfo[countryValue] || !countryInfo[countryValue].regions) {
+ return regionValue;
+ }
+
+ const regions = countryInfo[countryValue].regions;
+
+ let region = regions.find(nextRegion => nextRegion.value === regionValue);
+
+ if (region) {
+ return regionValue;
+ }
+
+ region = regions.find(nextRegion => nextRegion.label === regionValue);
+
+ if (region) {
+ return region.value;
+ } else {
+ return false;
+ }
+};
diff --git a/packages/vulcan-ui-material/lib/components/index.js b/packages/vulcan-ui-material/lib/components/index.js
new file mode 100644
index 000000000..cbee0bccb
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/index.js
@@ -0,0 +1,59 @@
+import './accounts/AccountsButton';
+import './accounts/AccountsButtons';
+import './accounts/AccountsField';
+import './accounts/AccountsFields';
+import './accounts/AccountsForm';
+import './accounts/AccountsPasswordOrService';
+import './accounts/AccountsSocialButtons';
+
+import './bonus/LoadMore';
+import './bonus/SearchInput';
+import './bonus/TooltipIntl';
+import './bonus/TooltipIconButton';
+
+import './core/Card';
+import './core/Datatable';
+import './core/EditButton';
+import './core/Loading';
+import './core/ModalTrigger';
+import './core/NewButton';
+
+import './forms/FormComponentInner';
+import './forms/FormErrors';
+//import './forms/FormGroup';
+import './forms/FormGroupNone';
+import './forms/FormGroupWithLine';
+import './forms/FormNested';
+import './forms/FormNestedArrayLayout';
+import './forms/FormNestedDivider';
+import './forms/FormNestedFoot';
+import './forms/FormNestedHead';
+import './forms/FormSubmit';
+import './forms/controls/Checkbox';
+import './forms/controls/CheckboxGroup';
+import './forms/controls/CountrySelect';
+import './forms/controls/Date';
+import './forms/controls/DateRdt';
+import './forms/controls/DateTime';
+import './forms/controls/DateTimeRdt';
+import './forms/controls/Default';
+import './forms/controls/Email';
+import './forms/controls/Number';
+import './forms/controls/PostalCode';
+import './forms/controls/RadioGroup';
+import './forms/controls/RegionSelect';
+import './forms/controls/Select';
+import './forms/controls/Textarea';
+import './forms/controls/Time';
+import './forms/controls/TimeRdt';
+import './forms/controls/Url';
+
+import './theme/ThemeStyles';
+
+import './ui/Button';
+import './ui/Alert';
+
+import './upload/UploadImage';
+import './upload/UploadInner';
+
+export * from './forms/controls/countries';
diff --git a/packages/vulcan-ui-material/lib/components/theme/JssCleanup.jsx b/packages/vulcan-ui-material/lib/components/theme/JssCleanup.jsx
new file mode 100644
index 000000000..4d344294b
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/theme/JssCleanup.jsx
@@ -0,0 +1,23 @@
+import React, { PureComponent } from 'react';
+
+
+class JssCleanup extends PureComponent {
+
+
+ // Remove the server-side injected CSS.
+ componentDidMount() {
+ if (!document || !document.getElementById) return;
+
+ const jssStyles = document.getElementById('jss-server-side');
+ if (jssStyles && jssStyles.parentNode) {
+// jssStyles.parentNode.removeChild(jssStyles);
+ }
+ }
+
+ render() {
+ return this.props.children;
+ }
+}
+
+
+export default JssCleanup;
diff --git a/packages/vulcan-ui-material/lib/components/theme/ThemeStyles.jsx b/packages/vulcan-ui-material/lib/components/theme/ThemeStyles.jsx
new file mode 100644
index 000000000..7441fc01b
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/theme/ThemeStyles.jsx
@@ -0,0 +1,232 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Components, registerComponent } from 'meteor/vulcan:core';
+import withTheme from '@material-ui/core/styles/withTheme';
+import withStyles from '@material-ui/core/styles/withStyles';
+import Typography from '@material-ui/core/Typography';
+import Grid from '@material-ui/core/Grid';
+import Paper from '@material-ui/core/Paper';
+import { getContrastRatio } from '@material-ui/core/styles/colorManipulator';
+import classNames from 'classnames';
+
+
+const describeTypography = (theme, className) => {
+ const typography = className ? theme.typography[className] : theme.typography;
+ const fontFamily = typography.fontFamily.split(',')[0];
+ return `${fontFamily} ${typography.fontWeight} ${typography.fontSize}px`;
+};
+
+
+const mainPalette = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900];
+const altPalette = ['A100', 'A200', 'A400', 'A700'];
+
+
+function getColorBlock(theme, classes, colorName, colorValue, colorTitle) {
+ const bgColor = theme.palette[colorName][colorValue];
+
+ let fgColor = theme.palette.common.black;
+ if (getContrastRatio(bgColor, fgColor) < 7) {
+ fgColor = theme.palette.common.white;
+ }
+
+ let blockTitle;
+ if (colorTitle) {
+ blockTitle = {colorName}
;
+ }
+
+ let rowStyle = {
+ backgroundColor: bgColor,
+ color: fgColor,
+ listStyle: 'none',
+ padding: 15,
+ };
+
+ if (colorValue.toString().indexOf('A1') === 0) {
+ rowStyle = {
+ ...rowStyle,
+ marginTop: 4,
+ };
+ }
+
+ return (
+
+ {blockTitle}
+
+ {colorValue}
+ {bgColor.toUpperCase()}
+
+
+ );
+}
+
+function getColorGroup(options) {
+ const { theme, classes, color, showAltPalette } = options;
+ const cssColor = color.replace(' ', '').replace(color.charAt(0), color.charAt(0).toLowerCase());
+ let colorsList = [];
+ colorsList = mainPalette.map(mainValue => getColorBlock(theme, classes, cssColor, mainValue));
+
+ if (showAltPalette) {
+ altPalette.forEach(altValue => {
+ colorsList.push(getColorBlock(theme, classes, cssColor, altValue));
+ });
+ }
+
+ return (
+
+ {getColorBlock(theme, classes, cssColor, 500, true)}
+
+ {colorsList}
+
+ );
+}
+
+
+const styles = theme => ({
+ root: {},
+ paper: {
+ padding: theme.spacing.unit * 3,
+ },
+ name: {
+ marginBottom: 60,
+ },
+ blockSpace: {
+ height: 4,
+ },
+ colorContainer: {
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ },
+ colorGroup: {
+ padding: '16px 0',
+ margin: '0 15px 0 0',
+ flexGrow: 1,
+ [theme.breakpoints.up('sm')]: {
+ flexGrow: 0,
+ },
+ },
+ colorValue: {
+ ...theme.typography.caption,
+ color: 'inherit',
+ },
+});
+
+
+const latin = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur justo quam, ' +
+ 'pellentesque ultrices ex a, aliquet porttitor ante. Donec tellus arcu, viverra ut lorem id, ' +
+ 'ultrices ultricies enim. Donec enim metus, sollicitudin id lobortis id, iaculis ut arcu. ' +
+ 'Maecenas sollicitudin congue nisi. Donec convallis, ipsum ac ultricies dignissim, orci ex ' +
+ 'efficitur lectus, ac lacinia risus nunc at diam. Nam gravida bibendum lectus. Donec ' +
+ 'scelerisque sem nec urna vestibulum vehicula.';
+
+
+const ThemeStyles = ({ theme, classes }) => {
+ return (
+
+
+
+
+ h1: {describeTypography(theme, 'h1')}
+
+
+ h2: {describeTypography(theme, 'h2')}
+
+
+ h3: {describeTypography(theme, 'h3')}
+
+
+ h4: {describeTypography(theme, 'h4')}
+
+
+
+
+
+
+ Headline: {describeTypography(theme, 'h5')}
+
+
+ Title: {describeTypography(theme, 'h6')}
+
+
+ Subtitle1: {describeTypography(theme, 'subtitle1')}
+
+
+ Body 1: {describeTypography(theme, 'body1')} - {latin}
+
+
+ {latin}
+
+
+ Body 2: {describeTypography(theme, 'body2')} - {latin}
+
+
+ {latin}
+
+
+ Caption: {describeTypography(theme, 'caption')}
+
+
+ Base: {describeTypography(theme)} - {latin}
+
+
+ Button - {describeTypography(theme)}
+
+
+
+
+
+ {
+ getColorGroup({
+ theme,
+ classes,
+ color: 'primary',
+ showAltPalette: true,
+ })
+ }
+
+
+
+ {
+ getColorGroup({
+ theme,
+ classes,
+ color: 'secondary',
+ showAltPalette: true,
+ })
+ }
+
+
+
+ {
+ getColorGroup({
+ theme,
+ classes,
+ color: 'error',
+ showAltPalette: true,
+ })
+ }
+
+
+
+ {
+ getColorGroup({
+ theme,
+ classes,
+ color: 'background',
+ showAltPalette: true,
+ })
+ }
+
+
+
+ );
+};
+
+
+ThemeStyles.propTypes = {
+ theme: PropTypes.object.isRequired,
+ classes: PropTypes.object.isRequired,
+};
+
+
+registerComponent('ThemeStyles', ThemeStyles, [withTheme, null], [withStyles, styles]);
diff --git a/packages/vulcan-ui-material/lib/components/ui/Alert.jsx b/packages/vulcan-ui-material/lib/components/ui/Alert.jsx
new file mode 100644
index 000000000..7a1254f5d
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/ui/Alert.jsx
@@ -0,0 +1,31 @@
+/**
+ * @Author: Apollinaire Lecocq
+ * @Date: 09-01-19
+ * @Last modified by: apollinaire
+ * @Last modified time: 10-01-19
+ */
+import React from 'react';
+import withStyles from '@material-ui/core/styles/withStyles';
+import { registerComponent } from 'meteor/vulcan:core';
+
+import Card from '@material-ui/core/Card';
+import CardContent from '@material-ui/core/CardContent';
+
+const AlertStyle = theme => ({
+ error: {
+ color: theme.palette.error.main,
+ backgroundColor: theme.palette.error[100],
+ fontFamily: theme.typography.fontFamily,
+ },
+ other: {
+ fontFamily: theme.typography.fontFamily,
+ },
+});
+
+const Alert = ({ children, variant, classes, ...rest }) => (
+
+ {children}
+
+);
+
+registerComponent({ name: 'Alert', component: Alert, hocs: [[withStyles, AlertStyle]] });
diff --git a/packages/vulcan-ui-material/lib/components/ui/Button.jsx b/packages/vulcan-ui-material/lib/components/ui/Button.jsx
new file mode 100644
index 000000000..23ea96d84
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/ui/Button.jsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import { Components, registerComponent } from 'meteor/vulcan:core';
+import MuiButton from '@material-ui/core/Button';
+import MuiIconButton from '@material-ui/core/IconButton';
+
+
+const Button = ({ children, color, variant, size, iconButton, ...rest }) => {
+ switch(variant) {
+ case 'success':
+ color = 'primary';
+ variant = null;
+ break;
+ case 'danger':
+ color = 'default';
+ variant = null;
+ break;
+ case 'inverse':
+ color = 'inherit';
+ variant = null;
+ break;
+ }
+
+ switch(size) {
+ case 'xsmall':
+ size = 'small';
+ break;
+ case 'small':
+ size = 'medium';
+ break;
+ case 'large':
+ size = 'large';
+ break;
+ }
+
+ if (iconButton) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ return (
+
+ {children}
+
+ );
+};
+
+
+registerComponent('Button', Button);
diff --git a/packages/vulcan-ui-material/lib/components/upload/UploadImage.jsx b/packages/vulcan-ui-material/lib/components/upload/UploadImage.jsx
new file mode 100755
index 000000000..6904e2fba
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/upload/UploadImage.jsx
@@ -0,0 +1,117 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { Components, registerComponent } from 'meteor/vulcan:lib';
+import { FormattedMessage } from 'meteor/vulcan:i18n';
+import withStyles from '@material-ui/core/styles/withStyles';
+import IconButton from '@material-ui/core/IconButton';
+import DeleteIcon from 'mdi-material-ui/Delete';
+import classNames from 'classnames';
+
+/**
+ * Used by UploadInner to display a single image
+ */
+const styles = theme => ({
+
+ uploadImage: {
+ textAlign: 'center',
+ marginBottom: theme.spacing.unit * -1,
+ marginLeft: theme.spacing.unit * 0.5,
+ marginRight: theme.spacing.unit * 0.5,
+ },
+
+ uploadImageContents: {
+ position: 'relative',
+ },
+
+ uploadImageImg: {
+ display: 'block',
+ maxWidth: 150,
+ maxHeight: 150,
+ },
+
+ uploadLoading: {
+ position: 'absolute',
+ top: 0,
+ bottom: 0,
+ left: 0,
+ right: 0,
+ background: 'rgba(255,255,255,0.8)',
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ span: {
+ display: 'block',
+ fontSize: '1.5rem',
+ }
+ },
+
+ deleteButton: {
+ }
+
+});
+
+
+class UploadImage extends PureComponent {
+
+ constructor (props) {
+ super(props);
+ this.handleClear = this.handleClear.bind(this);
+ }
+
+ handleClear (event) {
+ event.preventDefault();
+ this.props.clearImage(this.props.index);
+ }
+
+ // Get the URL of an image or the first in an array of images
+ getImageUrl (imageOrImageArray) {
+ // if image is actually an array of formats, use first format
+ const image = Array.isArray(imageOrImageArray) ? imageOrImageArray[0] : imageOrImageArray;
+
+ // if image is an object, return secure_url; else return image itself
+ return typeof image === 'string' ? image : image.secure_url;
+ }
+
+ render () {
+ const { loading, error, image, style, classes } = this.props;
+
+ return (
+
+
+
+
+
})
+ {
+ loading &&
+
+
+
+
+ }
+
+
+
+
+
+
+
+ );
+ }
+}
+
+
+UploadImage.propTypes = {
+ clearImage: PropTypes.func.isRequired,
+ index: PropTypes.number.isRequired,
+ image: PropTypes.oneOfType([PropTypes.string, PropTypes.array, PropTypes.object]),
+ loading: PropTypes.bool,
+ error: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
+ style: PropTypes.object,
+ classes: PropTypes.object.isRequired,
+};
+
+
+UploadImage.displayName = 'UploadImageMui';
+
+
+registerComponent('UploadImage', UploadImage, [withStyles, styles]);
diff --git a/packages/vulcan-ui-material/lib/components/upload/UploadInner.jsx b/packages/vulcan-ui-material/lib/components/upload/UploadInner.jsx
new file mode 100755
index 000000000..1392ec066
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/components/upload/UploadInner.jsx
@@ -0,0 +1,181 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Components, registerComponent, getComponent } from 'meteor/vulcan:lib';
+import Dropzone from 'react-dropzone';
+import withStyles from '@material-ui/core/styles/withStyles';
+import { FormattedMessage } from 'meteor/vulcan:i18n';
+import FormControl from '@material-ui/core/FormControl';
+import FormLabel from '@material-ui/core/FormLabel';
+import FormHelperText from '@material-ui/core/FormHelperText';
+
+
+/*
+
+Material UI GUI for Cloudinary Image Upload component
+
+*/
+
+
+const styles = theme => ({
+
+ root: {},
+
+ label: {},
+
+ uploadField: {
+ marginTop: theme.spacing.unit,
+ },
+
+ dropzoneBase: {
+ borderWidth: 3,
+ borderStyle: 'dashed',
+ borderColor: theme.palette.background[900],
+ backgroundColor: theme.palette.background[100],
+ color: theme.palette.common.lightBlack,
+ padding: '30px 60px',
+ transition: 'all 0.5s',
+ cursor: 'pointer',
+ position: 'relative',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ '&[aria-disabled="false"]:hover': {
+ color: theme.palette.common.midBlack,
+ borderColor: theme.palette.background['A200'],
+ }
+ },
+
+ dropzoneActive: {
+ borderStyle: 'solid',
+ borderColor: theme.palette.status.info,
+ },
+
+ dropzoneReject: {
+ borderStyle: 'solid',
+ borderColor: theme.palette.status.danger,
+ },
+
+ uploadState: {},
+
+ uploadImages: {
+ border: `1px solid ${theme.palette.background[500]}`,
+ backgroundColor: theme.palette.background[100],
+ display: 'flex',
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ justifyContent: 'center',
+ paddingTop: theme.spacing.unit,
+ paddingRight: theme.spacing.unit * 0.5,
+ paddingBottom: theme.spacing.unit,
+ paddingLeft: theme.spacing.unit * 0.5,
+ },
+});
+
+
+const UploadInner = (props) => {
+ const {
+ uploading,
+ images,
+ disabled,
+ maxCount,
+ label,
+ help,
+ options,
+ enableMultiple,
+ onDrop,
+ isDeleted,
+ clearImage,
+ classes
+ } = props;
+
+ const UploadImage = getComponent(options.uploadImageComponentName || 'UploadImage');
+
+ return (
+
+
+
+ {label}
+
+ {
+ help &&
+
+ {help}
+ }
+
+ {
+ (disabled && !enableMultiple)
+ ?
+ null
+ :
+
+
+
+
+ {uploading && (
+
+
+
+
+
+ )}
+
+ }
+
+ {!!images.length && (
+
+
+ {images.map(
+ (image, index) =>
+ !isDeleted(index) && (
+
+ )
+ )}
+
+
+ )}
+
+
+
+ );
+};
+
+
+UploadInner.propTypes = {
+ uploading: PropTypes.bool,
+ images: PropTypes.array.isRequired,
+ disabled: PropTypes.bool,
+ maxCount: PropTypes.number.isRequired,
+ label: PropTypes.string,
+ help: PropTypes.string,
+ options: PropTypes.object.isRequired,
+ enableMultiple: PropTypes.bool,
+ onDrop: PropTypes.func.isRequired,
+ isDeleted: PropTypes.func.isRequired,
+ clearImage: PropTypes.func.isRequired,
+ classes: PropTypes.object.isRequired,
+};
+
+
+UploadInner.displayName = 'UploadInnerMui';
+
+
+registerComponent('UploadInner', UploadInner, [withStyles, styles]);
diff --git a/packages/vulcan-ui-material/lib/example/Header.jsx b/packages/vulcan-ui-material/lib/example/Header.jsx
new file mode 100644
index 000000000..7ae557a4f
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/example/Header.jsx
@@ -0,0 +1,98 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import AppBar from '@material-ui/core/AppBar';
+import Toolbar from '@material-ui/core/Toolbar';
+import IconButton from '@material-ui/core/IconButton';
+import Typography from '@material-ui/core/Typography';
+import MenuIcon from 'mdi-material-ui/Menu';
+import ChevronLeftIcon from 'mdi-material-ui/ChevronLeft';
+import withStyles from '@material-ui/core/styles/withStyles';
+import { getSetting, Components, registerComponent } from 'meteor/vulcan:core';
+import classNames from 'classnames';
+
+
+const drawerWidth = 240;
+const topBarHeight = 100;
+
+
+const styles = theme => ({
+ appBar: {
+ position: 'absolute',
+ transition: theme.transitions.create(['margin', 'width'], {
+ easing: theme.transitions.easing.sharp,
+ duration: theme.transitions.duration.leavingScreen,
+ }),
+ },
+ appBarShift: {
+ marginLeft: drawerWidth,
+ width: `calc(100% - ${drawerWidth}px)`,
+ transition: theme.transitions.create(['margin', 'width'], {
+ easing: theme.transitions.easing.easeOut,
+ duration: theme.transitions.duration.enteringScreen,
+ }),
+ },
+ toolbar: {
+ height: `${topBarHeight}px`,
+ minHeight: `${topBarHeight}px`,
+ },
+ headerMid: {
+ flexGrow: 1,
+ display: 'flex',
+ alignItems: 'center',
+ '& h1': {
+ margin: '0 24px 0 0',
+ fontSize: '18px',
+ lineHeight: 1,
+ }
+ },
+ menuButton: {
+ marginRight: theme.spacing.unit * 3,
+ },
+});
+
+
+const Header = (props, context) => {
+ const classes = props.classes;
+ const isSideNavOpen = props.isSideNavOpen;
+ const toggleSideNav = props.toggleSideNav;
+
+ const siteTitle = getSetting('title', 'My App');
+
+ return (
+
+
+
+ toggleSideNav()}
+ className={classNames(classes.menuButton)}
+ color="inherit"
+ >
+ {isSideNavOpen ? : }
+
+
+
+
+
+ {siteTitle}
+
+
+
+
+
+
+ );
+};
+
+
+Header.propTypes = {
+ classes: PropTypes.object.isRequired,
+ isSideNavOpen: PropTypes.bool,
+ toggleSideNav: PropTypes.func,
+};
+
+
+Header.displayName = 'Header';
+
+
+registerComponent('Header', Header, [withStyles, styles]);
diff --git a/packages/vulcan-ui-material/lib/example/Layout.jsx b/packages/vulcan-ui-material/lib/example/Layout.jsx
new file mode 100644
index 000000000..63c472239
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/example/Layout.jsx
@@ -0,0 +1,137 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Drawer from '@material-ui/core/Drawer';
+import AppBar from '@material-ui/core/AppBar';
+import Toolbar from '@material-ui/core/Toolbar';
+import { Components, replaceComponent, Utils } from 'meteor/vulcan:core';
+import withStyles from '@material-ui/core/styles/withStyles';
+import classNames from 'classnames';
+
+
+const drawerWidth = 240;
+const topBarHeight = 100;
+
+
+const styles = theme => {
+ const contentPadding = theme.spacing.unit * 8;
+
+ return {
+ '@global': {
+ html: {
+ background: theme.palette.background.default,
+ WebkitFontSmoothing: 'antialiased',
+ MozOsxFontSmoothing: 'grayscale',
+ overflow: 'hidden',
+ },
+ body: {
+ margin: 0,
+ },
+ },
+ root: {
+ width: '100%',
+ zIndex: 1,
+ overflow: 'hidden',
+ },
+ appFrame: {
+ position: 'relative',
+ display: 'flex',
+ height: '100vh',
+ alignItems: 'stretch',
+ },
+ drawerPaper: {
+ position: 'relative',
+ width: drawerWidth,
+ backgroundColor: theme.palette.background[200],
+ },
+ drawerHeader: {
+ height: `${topBarHeight}px !important`,
+ minHeight: `${topBarHeight}px !important`,
+ position: 'relative !important',
+ },
+ content: {
+ padding: contentPadding,
+ width: '100%',
+ marginLeft: -drawerWidth,
+ flexGrow: 1,
+ backgroundColor: theme.palette.background.default,
+ color: theme.palette.text.primary,
+ transition: theme.transitions.create('margin', {
+ easing: theme.transitions.easing.sharp,
+ duration: theme.transitions.duration.leavingScreen,
+ }),
+ height: `calc(100% - ${topBarHeight}px - ${contentPadding * 2}px)`,
+ marginTop: topBarHeight,
+ overflowY: 'scroll',
+ },
+ mainShift: {
+ marginLeft: 0,
+ transition: theme.transitions.create('margin', {
+ easing: theme.transitions.easing.easeOut,
+ duration: theme.transitions.duration.enteringScreen,
+ }),
+ },
+ };
+};
+
+
+
+ class Layout extends React.Component {
+ state = {
+ isOpen: { sideNav: true }
+ };
+
+ toggle = (item, openOrClose) => {
+ const newState = { isOpen: {} };
+ newState.isOpen[item] = typeof openOrClose === 'string' ?
+ openOrClose === 'open' :
+ !this.state.isOpen[item];
+ this.setState(newState);
+ };
+
+ render = () => {
+ const routeName = Utils.slugify(this.props.currentRoute.name);
+ const classes = this.props.classes;
+ const isOpen = this.state.isOpen;
+
+ return (
+
+
+
+
+ this.toggle('sideNav', openOrClose)} />
+
+
+
+
+
+
+
+
+
+
+ {this.props.children}
+
+
+
+
+
+
+ );
+ };
+ }
+
+
+ Layout.propTypes = {
+ classes: PropTypes.object.isRequired,
+ children: PropTypes.node,
+ };
+
+
+ Layout.displayName = 'Layout';
+
+
+ replaceComponent('Layout', Layout, [withStyles, styles]);
diff --git a/packages/vulcan-ui-material/lib/example/SideNavigation.jsx b/packages/vulcan-ui-material/lib/example/SideNavigation.jsx
new file mode 100644
index 000000000..745821389
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/example/SideNavigation.jsx
@@ -0,0 +1,104 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Components, registerComponent, withCurrentUser } from 'meteor/vulcan:core';
+import { withRouter } from 'react-router';
+import List from '@material-ui/core/List';
+import ListItem from '@material-ui/core/ListItem';
+import ListItemIcon from '@material-ui/core/ListItemIcon';
+import ListItemText from '@material-ui/core/ListItemText';
+import Divider from '@material-ui/core/Divider';
+import Collapse from '@material-ui/core/Collapse';
+import ExpandLessIcon from 'mdi-material-ui/ChevronUp';
+import ExpandMoreIcon from 'mdi-material-ui/ChevronDown';
+import LockIcon from 'mdi-material-ui/Lock';
+import UsersIcon from 'mdi-material-ui/AccountMultiple';
+import ThemeIcon from 'mdi-material-ui/Palette';
+import HomeIcon from 'mdi-material-ui/Home';
+import withStyles from '@material-ui/core/styles/withStyles';
+import Users from 'meteor/vulcan:users';
+
+
+const styles = theme => ({
+ root: {},
+ nested: {
+ paddingLeft: theme.spacing.unit * 4,
+ },
+});
+
+
+class SideNavigation extends React.Component {
+ state = {
+ isOpen: { admin: false }
+ };
+
+ toggle = (item) => {
+ const newState = { isOpen: {} };
+ newState.isOpen[item] = !this.state.isOpen[item];
+ this.setState(newState);
+ };
+
+ render () {
+ const { currentUser, classes, history } = this.props;
+ const isOpen = this.state.isOpen;
+
+ return (
+
+
+
+ {history.push('/');}}>
+
+
+
+
+
+
+
+ {
+ Users.isAdmin(currentUser) &&
+
+
+
+
+ this.toggle('admin')}>
+
+
+
+
+ {isOpen.admin ? : }
+
+
+ {history.push('/admin');}}>
+
+
+
+
+
+ {history.push('/theme');}}>
+
+
+
+
+
+
+
+
+ }
+
+
+ );
+ }
+}
+
+
+SideNavigation.propTypes = {
+ classes: PropTypes.object.isRequired,
+ currentUser: PropTypes.object,
+};
+
+
+SideNavigation.displayName = 'SideNavigation';
+
+
+registerComponent('SideNavigation', SideNavigation, [withStyles, styles], withCurrentUser, withRouter);
diff --git a/packages/vulcan-ui-material/lib/modules/components.js b/packages/vulcan-ui-material/lib/modules/components.js
new file mode 100644
index 000000000..4aba840ab
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/modules/components.js
@@ -0,0 +1 @@
+export * from '../components';
\ No newline at end of file
diff --git a/packages/vulcan-ui-material/lib/modules/index.js b/packages/vulcan-ui-material/lib/modules/index.js
new file mode 100644
index 000000000..e614e7b6b
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/modules/index.js
@@ -0,0 +1,5 @@
+export * from './components';
+export * from './themes';
+export JssCleanup from '../components/theme/JssCleanup';
+import './sampleTheme';
+import './routes';
diff --git a/packages/vulcan-ui-material/lib/modules/routes.js b/packages/vulcan-ui-material/lib/modules/routes.js
new file mode 100755
index 000000000..55d267e77
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/modules/routes.js
@@ -0,0 +1,10 @@
+import { addRoute } from 'meteor/vulcan:core';
+
+//Only create route on dev mode, not production.
+if (Meteor.isDevelopment) {
+ addRoute({
+ name: 'theme',
+ path: '/theme',
+ componentName: 'ThemeStyles',
+ });
+}
diff --git a/packages/vulcan-ui-material/lib/modules/sampleTheme.js b/packages/vulcan-ui-material/lib/modules/sampleTheme.js
new file mode 100644
index 000000000..a382078a7
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/modules/sampleTheme.js
@@ -0,0 +1,76 @@
+import { registerTheme } from './themes';
+import indigo from '@material-ui/core/colors/indigo';
+import blue from '@material-ui/core/colors/blue';
+import red from '@material-ui/core/colors/red';
+
+
+/** @ignore */
+
+/**
+ *
+ * Sample theme to get you out of the gate quickly
+ *
+ * For a complete list of configuration variables see:
+ * https://material-ui.com/customization/themes/
+ *
+ */
+
+
+const theme = {
+
+ palette: {
+ primary: indigo,
+ secondary: blue,
+ error: red,
+ },
+
+ utils: {
+
+ tooltipEnterDelay: 700,
+
+ errorMessage: {
+ textAlign: 'center',
+ backgroundColor: red[500],
+ color: 'white',
+ borderRadius: '4px',
+ fontWeight: 'bold',
+ },
+
+ denseTable: {
+ '& > thead > tr > th, & > tbody > tr > td': {
+ padding: '4px 16px 4px 16px',
+ },
+ '& > thead > tr > th:last-child, & > tbody > tr > td:last-child': {
+ paddingRight: '16px',
+ },
+ },
+
+ flatTable: {
+ '& > thead > tr > th, & > tbody > tr > td': {
+ padding: '4px 16px 4px 16px',
+ whiteSpace: 'nowrap',
+ },
+ '& > thead > tr > th:last-child, & > tbody > tr > td:last-child': {
+ paddingRight: '16px',
+ },
+ },
+
+ denserTable: {
+ '& > thead > tr, & > tbody > tr': {
+ height: '40px',
+ },
+ '& > thead > tr > th, & > tbody > tr > td': {
+ padding: '4px 16px 4px 16px',
+ whiteSpace: 'nowrap',
+ },
+ '& > thead > tr > th:last-child, & > tbody > tr > td:last-child': {
+ paddingRight: '16px',
+ },
+ },
+
+ },
+
+};
+
+
+registerTheme('Sample', theme);
diff --git a/packages/vulcan-ui-material/lib/modules/themes.js b/packages/vulcan-ui-material/lib/modules/themes.js
new file mode 100644
index 000000000..d829dc36d
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/modules/themes.js
@@ -0,0 +1,67 @@
+/** @module vulcan-material-ui */
+
+import createMuiTheme from '@material-ui/core/styles/createMuiTheme';
+import { registerSetting, getSetting } from 'meteor/vulcan:core';
+
+
+registerSetting('muiTheme', 'Sample', 'Material UI theme used by erikdakota:vulcan-material-ui');
+
+
+export const ThemesTable = {}; // storage for info about themes
+
+
+/**
+ * Register a theme with a name
+ *
+ * @param {String} name The name of the theme to register
+ * @param {Object} theme The theme object - see defaultTheme.js
+ *
+ */
+export const registerTheme = (name, theme) => {
+ const themeInfo = {
+ name,
+ theme,
+ };
+
+ ThemesTable[name] = themeInfo;
+};
+
+
+/**
+ * Get a theme registered with registerTheme()
+ *
+ * @param {String} name The name of the theme to get
+ *
+ * @returns {Object} A theme object
+ */
+export const getTheme = (name) => {
+ const themeInfo = ThemesTable[name];
+ if (!themeInfo) return null;
+ themeInfo.theme.typography = { ...themeInfo.theme.typography, useNextVariants: true };
+ return createMuiTheme(themeInfo.theme);
+};
+
+/**
+ * Get the raw theme object registered with registerTheme()
+ *
+ * @param {String} name The name of the theme to get
+ *
+ * @returns {Object} The object passed to registerTheme
+ */
+
+export const getRawTheme = (name) => {
+ const themeInfo = ThemesTable[name];
+ if (!themeInfo) return null;
+ return themeInfo.theme;
+};
+
+/**
+ * Get the theme specified in the 'muiTheme' setting
+ *
+ * @returns {Object}
+ */
+export const getCurrentTheme = () => {
+ const themeName = getSetting('muiTheme', 'Sample');
+ const theme = getTheme(themeName);
+ return theme;
+};
diff --git a/packages/vulcan-ui-material/lib/server/main.js b/packages/vulcan-ui-material/lib/server/main.js
new file mode 100644
index 000000000..dadfe15cf
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/server/main.js
@@ -0,0 +1,2 @@
+export * from '../modules/index';
+import './wrapWithMuiTheme';
diff --git a/packages/vulcan-ui-material/lib/server/wrapWithMuiTheme.jsx b/packages/vulcan-ui-material/lib/server/wrapWithMuiTheme.jsx
new file mode 100644
index 000000000..32933c0e5
--- /dev/null
+++ b/packages/vulcan-ui-material/lib/server/wrapWithMuiTheme.jsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import { addCallback } from 'meteor/vulcan:core';
+import JssProvider from 'react-jss/lib/JssProvider';
+import MuiThemeProvider from '@material-ui/core/styles/MuiThemeProvider';
+import createGenerateClassName from '@material-ui/core/styles/createGenerateClassName';
+import { getCurrentTheme } from '../modules/themes';
+import { SheetsRegistry } from 'react-jss/lib/jss';
+import JssCleanup from '../components/theme/JssCleanup';
+
+
+function wrapWithMuiTheme (app, {context }) {
+ const sheetsRegistry = new SheetsRegistry();
+ context.sheetsRegistry = sheetsRegistry;
+
+ const sheetsManager = new Map();
+
+ const theme = getCurrentTheme();
+
+ const generateClassName = createGenerateClassName({ seed: '' });
+
+ return (
+
+
+
+ {app}
+
+
+
+ );
+}
+
+
+function injectJss(sink, { context }) {
+ const sheets = context.sheetsRegistry.toString();
+ sink.appendToHead(
+ ``
+ );
+ return sink;
+}
+
+
+addCallback('router.server.wrapper', wrapWithMuiTheme);
+addCallback('router.server.postRender', injectJss);
diff --git a/packages/vulcan-ui-material/package.js b/packages/vulcan-ui-material/package.js
new file mode 100644
index 000000000..5b4caf370
--- /dev/null
+++ b/packages/vulcan-ui-material/package.js
@@ -0,0 +1,25 @@
+Package.describe({
+ name: 'vulcan:ui-material',
+ version: '1.13.0',
+ summary: 'Replacement for Vulcan (http://vulcanjs.org/) components using material-ui',
+ documentation: 'README.md'
+});
+
+Package.onUse(function (api) {
+ api.versionsFrom('METEOR@1.6');
+
+ api.use([
+ 'ecmascript',
+ 'vulcan:core@1.12.8',
+ 'vulcan:accounts@1.12.8',
+ 'vulcan:forms@1.12.8',
+ ]);
+
+ api.addFiles([
+ 'accounts.css',
+ 'forms.css',
+ ], ['client', 'server']);
+
+ api.mainModule('lib/client/main.js', 'client');
+ api.mainModule('lib/server/main.js', 'server');
+});
\ No newline at end of file
diff --git a/packages/vulcan-ui-material/readme.md b/packages/vulcan-ui-material/readme.md
new file mode 100644
index 000000000..5e2d912f9
--- /dev/null
+++ b/packages/vulcan-ui-material/readme.md
@@ -0,0 +1,209 @@
+# vulcan:ui-material 1.12.8_13
+
+Package initially created by [Erik Dakoda](https://github.com/ErikDakoda) ([`erikdakoda:vulcan-material-ui`](https://github.com/ErikDakoda/vulcan-material-ui))
+
+
+Replacement for [Vulcan](http://vulcanjs.org/) components using [Material-UI](https://material-ui.com/).
+
+
+There are some nice bonus features like a CountrySelect with autocomplete and theming.
+
+All components in vulcan:ui-bootstrap, vulcan:forms and vulcan:accounts have been implemented except for Icon.
+
+## Installation
+
+To add vulcan-material-ui to an existing Vulcan project, enter the following:
+
+``` sh
+meteor add vulcan:ui-material
+
+meteor npm install --save @material-ui/core@3.1.0
+meteor npm install --save react-jss
+meteor npm install --save mdi-material-ui
+meteor npm install --save react-autosuggest
+meteor npm install --save autosuggest-highlight
+meteor npm install --save react-isolated-scroll
+meteor npm install --save-exact react-keyboard-event-handler@1.3.2
+#meteor npm install --save autosize-input
+meteor npm install --save moment-timezone
+```
+
+> NOTE: If you want to avoid deprecation warnings added in MUI versions after 3.1.0, you can lock MUI to the currently supported version using `meteor npm install --save @material-ui/core@3.1.0`. Don't for get to remove or update the version number when you update this package in the future.
+
+
+> IMPORTANT: Please note that I have abandoned material-ui-icons in favor of mdi-material-ui because it has a much larger [selection of icons](https://materialdesignicons.com/).
+
+This package no longer depends on `vulcan:ui-boostrap`, so you can remove it.
+
+To activate the example layout copy the three components to your project and import them:
+
+``` javascript
+import './example/Header',
+import './example/Layout',
+import './example/SideNavigation',
+```
+
+## Theming
+
+For an example theme see `modules/sampleTheme.js`. For a complete list of values you can customize,
+see the [MUI Default Theme](https://material-ui-next.com/customization/default-theme/).
+
+Register your theme in the Vulcan environment by giving it a name: `registerTheme('MyTheme', theme);`.
+You can have multiple themes registered and you can specify which one to use in your settings file using the `muiTheme` **public** setting.
+
+In addition to the Material UI spec, I use a `utils` section in my themes where I place global variables for reusable styles.
+For example the sample theme contains
+
+```
+const theme = {
+
+ . . .
+
+ utils: {
+
+ tooltipEnterDelay: 700,
+
+ errorMessage: {
+ textAlign: 'center',
+ backgroundColor: red[500],
+ color: 'white',
+ borderRadius: '4px',
+ },
+
+ . . .
+
+ // additional utils definitions go here
+
+ },
+
+};
+```
+
+You can use tooltipEnterDelay (or any other variable you define in utils) anywhere you include the withTheme HOC. See `/components/bonus/TooltipIconButton.jsx` for an example.
+
+You can use errorMessage (or any other style fragment you define in utils) anywhere you include the withStyles HOC. See `/components/accounts/Form.jsx` for an example.
+
+## Server Side Rendering (SSR)
+
+Material UI and Vulcan support SSR, but this is a complex beast with pitfalls. Sometimes you will see a warning like this:
+
+`Warning: Prop className did not match. Server: "MuiChip-label-131" Client: "MuiChip-label-130"`
+
+Sometimes the React rendered on the server and the client don't match exactly and this causes a problem with [JSS](https://material-ui-next.com/customization/css-in-js/#jss). This is a complicated issue that has multiple causes and I will be working on solving each of the issues causing this over time.
+
+Your pages should still render correctly, but there may be a blink and redraw when the first page after SSR loads in the browser.
+
+In your own code, make sure that your components will render the same on the server and the client. This means not referring to client-side object such as `document` or `jQuery`. If you have a misbehaving component, try wrapping it with [react-no-ssr](https://github.com/kadirahq/react-no-ssr).
+
+## Form Controls
+
+You can pass a couple of extra options to inputs from the `form` property of your schema:
+
+``` javascript
+ userKey: {
+ type: String,
+ label: 'User key',
+ description: 'The user’s key',
+ optional: true,
+ hidden: function ({ document }) {
+ return !document.platformId || !document.usePlatformApp;
+ },
+ inputProperties: {
+ autoFocus: true, // focus this input when the form loads
+ addonBefore: , // adorn the start of the input
+ addonAfter: , // adorn the end of the input
+ inputClassName: 'halfWidthLeft', // use 'halfWidthLeft' or 'halfWidthRight'
+ // to display two controls side by side
+ hideLabel: true, // hide the label
+ rows: 10, // for textareas you can specify the rows
+ variant: 'switch', // for checkboxgroups you can use either
+ // 'checkbox' (default) or 'switch'
+ inputProps: { step: 'any' } // Attributes applied to the input element, for
+ // ex pass the step attr to a number input
+ },
+ group: platformGroup,
+ canRead: ['members'],
+ canCreate: ['members'],
+ canUpdate: ['members'],
+ },
+```
+
+> Note: `form.getHidden` has been deprecated. Now you can just pass a function to `hidden`.
+
+## Form Groups
+
+You can pass a couple of extra options to form groups as well:
+
+``` javascript
+ const platformGroup: {
+ name: 'shops.platform',
+ order: 4,
+ startComponent: 'ShopsPlatformTitle', // component to put at the top of the form group
+ endComponent: 'ShopsConnectButtons', // component to put at the bottom of the form group
+ },
+```
+
+## DataTable
+
+You can pass the DataTable component an `editComponent` property in addition to or instead of `showEdit`. Here is a simplified example:
+
+``` javascript
+const AgendaJobActions = ({ collection, document }) => {
+ const scheduleAgent = () => {
+ Meteor.call('scheduleAgent', document.agentId);
+ };
+
+ return }
+ onClick={scheduleAgent}/>;
+};
+
+AgendaJobActionsInner.propTypes = {
+ collecion: PropTypes.object.isRequired,
+ document: PropTypes.object.isRequired,
+};
+
+
+```
+
+You can also control the spacing of the table cells using the `dense` property. Valid values are:
+
+| Value | Description |
+| ------- | ------------ |
+| dense | right cell padding of 16px instead of 56px |
+| flat | right cell padding of 16px and nowrap |
+| denser | right cell padding of 16px, nowrap, and row height of 40px instead of 56px |
+
+You can also use other string values, as long as you define a `utils` entry named the same + `Table`, for example `myCustomTable`. Check out the sample theme for examples.
+
+
+## CountrySelect
+
+There is an additional component, an autosuggest-based country select.
+
+``` javascript
+ country: {
+ type: String,
+ label: 'Country',
+ input: 'CountrySelect',
+ canRead: ['guests'],
+ canCreate: ['members'],
+ canUpdate: ['members'],
+ },
+```
+
+Countries are stored as their 2-letter country codes. I have included a helper function for displaying the country name:
+
+``` javascript
+import Typography from '@material-ui/core/Typography';
+import { getCountryLabel } from 'meteor/erikdakoda:vulcan-material-ui';
+
+
+ {getCountryLabel(supplier.country)}
+
+```
+
diff --git a/packages/vulcan-users/lib/server/callbacks.js b/packages/vulcan-users/lib/server/callbacks.js
index d1e719710..c044bef4f 100644
--- a/packages/vulcan-users/lib/server/callbacks.js
+++ b/packages/vulcan-users/lib/server/callbacks.js
@@ -39,7 +39,7 @@
addCallback('users.new.sync', usersMakeAdmin);
function usersEditGenerateHtmlBio (modifier) {
- if (modifier.$set && modifier.$set.bio) {
+ if (modifier.$set && 'bio' in modifier.$set) {
modifier.$set.htmlBio = Utils.sanitize(marked(modifier.$set.bio));
}
return modifier;
diff --git a/packages/vulcan-users/package.js b/packages/vulcan-users/package.js
index a5b953789..d0c05f902 100644
--- a/packages/vulcan-users/package.js
+++ b/packages/vulcan-users/package.js
@@ -1,14 +1,14 @@
Package.describe({
name: 'vulcan:users',
summary: 'Vulcan permissions.',
- 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');
diff --git a/packages/vulcan-voting/package.js b/packages/vulcan-voting/package.js
index 6430b8ee3..42ca87419 100644
--- a/packages/vulcan-voting/package.js
+++ b/packages/vulcan-voting/package.js
@@ -1,7 +1,7 @@
Package.describe({
name: 'vulcan:voting',
summary: 'Vulcan scoring package.',
- version: '1.12.17',
+ version: '1.13.0',
git: 'https://github.com/VulcanJS/Vulcan.git',
});
@@ -9,7 +9,7 @@ Package.onUse(function(api) {
api.versionsFrom('1.6.1');
api.use(
- ['fourseven:scss@4.10.0', 'vulcan:core@1.12.17', 'vulcan:i18n@1.12.17'],
+ ['fourseven:scss@4.10.0', 'vulcan:core@1.13.0', 'vulcan:i18n@1.13.0'],
['client', 'server'],
);