Merge branch 'devel' of github.com:VulcanJS/Vulcan into devel

This commit is contained in:
Valentin Boettcher 2019-04-04 11:54:31 +02:00
commit e93f4062d8
149 changed files with 7958 additions and 686 deletions

View file

@ -54,6 +54,7 @@
"single", "single",
"avoid-escape" "avoid-escape"
], ],
"react/display-name": 1,
"react/prop-types": 0, "react/prop-types": 0,
"semi": [1, "always"] "semi": [1, "always"]
}, },

272
package-lock.json generated
View file

@ -1,6 +1,6 @@
{ {
"name": "Vulcan", "name": "Vulcan",
"version": "1.12.16", "version": "1.12.17",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -330,13 +330,22 @@
"warning": "^3.0.0" "warning": "^3.0.0"
}, },
"dependencies": { "dependencies": {
"prop-types": { "loose-envify": {
"version": "15.6.2", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"requires": { "requires": {
"loose-envify": "^1.3.1", "js-tokens": "^3.0.0 || ^4.0.0"
"object-assign": "^4.1.1" }
},
"prop-types": {
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
"requires": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.8.1"
} }
} }
} }
@ -702,7 +711,6 @@
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
"integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=",
"optional": true,
"requires": { "requires": {
"kind-of": "^3.0.2", "kind-of": "^3.0.2",
"longest": "^1.0.1", "longest": "^1.0.1",
@ -898,11 +906,12 @@
} }
}, },
"apollo-errors": { "apollo-errors": {
"version": "1.5.1", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/apollo-errors/-/apollo-errors-1.5.1.tgz", "resolved": "https://registry.npmjs.org/apollo-errors/-/apollo-errors-1.9.0.tgz",
"integrity": "sha512-gYAceMzNJfF+mUHH2/4UcZTkZtDY54arCTKGbKa7WU5IXnTJ4V+P94wHodcDkLLHWpHL8SW1hEgjN5ZINcPb1w==", "integrity": "sha512-XVukHd0KLvgY6tNjsPS3/Re3U6RQlTKrTbIpqqeTMo2N34uQMr+H1UheV21o8hOZBAFosvBORVricJiP5vfmrw==",
"requires": { "requires": {
"es6-error": "^4.0.0" "assert": "^1.4.1",
"extendable-error": "^0.1.5"
} }
}, },
"apollo-link": { "apollo-link": {
@ -1289,6 +1298,14 @@
"safer-buffer": "~2.1.0" "safer-buffer": "~2.1.0"
} }
}, },
"assert": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz",
"integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=",
"requires": {
"util": "0.10.3"
}
},
"assert-err": { "assert-err": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/assert-err/-/assert-err-1.1.0.tgz", "resolved": "https://registry.npmjs.org/assert-err/-/assert-err-1.1.0.tgz",
@ -2384,7 +2401,7 @@
}, },
"cheerio": { "cheerio": {
"version": "0.22.0", "version": "0.22.0",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", "resolved": "http://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz",
"integrity": "sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=", "integrity": "sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=",
"requires": { "requires": {
"css-select": "~1.2.0", "css-select": "~1.2.0",
@ -2831,7 +2848,7 @@
}, },
"css-select": { "css-select": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", "resolved": "http://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
"integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=",
"requires": { "requires": {
"boolbase": "~1.0.0", "boolbase": "~1.0.0",
@ -3370,11 +3387,6 @@
"es6-symbol": "~3.1.1" "es6-symbol": "~3.1.1"
} }
}, },
"es6-error": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="
},
"es6-iterator": { "es6-iterator": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
@ -3977,6 +3989,11 @@
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
"integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ="
}, },
"extendable-error": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.5.tgz",
"integrity": "sha1-EiMIpwl7yJomOyxPvwiceBQOO20="
},
"extglob": { "extglob": {
"version": "0.3.2", "version": "0.3.2",
"resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz",
@ -6168,8 +6185,7 @@
"longest": { "longest": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
"integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc="
"optional": true
}, },
"loose-envify": { "loose-envify": {
"version": "1.3.1", "version": "1.3.1",
@ -6261,7 +6277,7 @@
"process": "^0.11.9", "process": "^0.11.9",
"punycode": "^1.4.1", "punycode": "^1.4.1",
"querystring-es3": "^0.2.1", "querystring-es3": "^0.2.1",
"readable-stream": "git+https://github.com/meteor/readable-stream.git", "readable-stream": "git+https://github.com/meteor/readable-stream.git#c688cdd193549919b840e8d72a86682d91961e12",
"stream-browserify": "^2.0.1", "stream-browserify": "^2.0.1",
"string_decoder": "^1.0.1", "string_decoder": "^1.0.1",
"timers-browserify": "^1.4.2", "timers-browserify": "^1.4.2",
@ -6711,12 +6727,19 @@
"version": "git+https://github.com/meteor/readable-stream.git#c688cdd193549919b840e8d72a86682d91961e12", "version": "git+https://github.com/meteor/readable-stream.git#c688cdd193549919b840e8d72a86682d91961e12",
"from": "git+https://github.com/meteor/readable-stream.git", "from": "git+https://github.com/meteor/readable-stream.git",
"requires": { "requires": {
"inherits": "~2.0.3", "inherits": "~2.0.1",
"isarray": "~1.0.0", "isarray": "~1.0.0",
"process-nextick-args": "~2.0.0", "process-nextick-args": "~1.0.6",
"safe-buffer": "~5.1.1", "safe-buffer": "^5.0.1",
"string_decoder": "~1.1.0", "string_decoder": "~1.0.0",
"util-deprecate": "~1.0.1" "util-deprecate": "~1.0.1"
},
"dependencies": {
"process-nextick-args": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
"integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M="
}
} }
}, },
"rimraf": { "rimraf": {
@ -7790,31 +7813,31 @@
} }
}, },
"react-bootstrap": { "react-bootstrap": {
"version": "1.0.0-beta.3", "version": "1.0.0-beta.5",
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.0.0-beta.3.tgz", "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.0.0-beta.5.tgz",
"integrity": "sha512-/eiSmRZE92q6m7uen3oAsOTGY4uBJkZDv32fwxUeyjesf834GUDaEhu1dzj10fmxSeVHW9O6UOKj9GkbwIIkMg==", "integrity": "sha512-Osm0OtTbYwfsT1rpu88ESWuAHZxfaHFNKFiW8w3w+6YY9/bLEPHbGRZA6W21fg5yvcuKN9hJKT857TTHgY7SoQ==",
"requires": { "requires": {
"@babel/runtime": "^7.0.0", "@babel/runtime": "^7.2.0",
"@react-bootstrap/react-popper": "1.2.1", "@react-bootstrap/react-popper": "1.2.1",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"dom-helpers": "^3.2.0", "dom-helpers": "^3.4.0",
"invariant": "^2.2.3", "invariant": "^2.2.3",
"keycode": "^2.1.2", "keycode": "^2.1.2",
"popper.js": "^1.14.3", "popper.js": "^1.14.6",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"prop-types-extra": "^1.1.0", "prop-types-extra": "^1.1.0",
"react-context-toolbox": "^1.2.3", "react-context-toolbox": "^2.0.2",
"react-overlays": "^1.0.0-beta.17", "react-overlays": "^1.0.0",
"react-prop-types": "^0.4.0", "react-prop-types": "^0.4.0",
"react-transition-group": "^2.4.0", "react-transition-group": "^2.5.1",
"uncontrollable": "^6.0.0", "uncontrollable": "^6.0.0",
"warning": "^4.0.1" "warning": "^4.0.1"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": { "@babel/runtime": {
"version": "7.2.0", "version": "7.3.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.2.0.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.4.tgz",
"integrity": "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg==", "integrity": "sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g==",
"requires": { "requires": {
"regenerator-runtime": "^0.12.0" "regenerator-runtime": "^0.12.0"
} }
@ -7824,6 +7847,14 @@
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
"integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
}, },
"dom-helpers": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz",
"integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==",
"requires": {
"@babel/runtime": "^7.1.2"
}
},
"invariant": { "invariant": {
"version": "2.2.4", "version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@ -7833,12 +7864,23 @@
} }
}, },
"prop-types": { "prop-types": {
"version": "15.6.2", "version": "15.7.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
"integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==", "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
"requires": { "requires": {
"loose-envify": "^1.3.1", "loose-envify": "^1.4.0",
"object-assign": "^4.1.1" "object-assign": "^4.1.1",
"react-is": "^16.8.1"
},
"dependencies": {
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
}
} }
}, },
"regenerator-runtime": { "regenerator-runtime": {
@ -7847,9 +7889,9 @@
"integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==" "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg=="
}, },
"warning": { "warning": {
"version": "4.0.2", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.2.tgz", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-wbTp09q/9C+jJn4KKJfJfoS6VleK/Dti0yqWSm6KMvJ4MRCXFQNapHuJXutJIrWV0Cf4AhTdeIe4qdKHR1+Hug==", "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"requires": { "requires": {
"loose-envify": "^1.0.0" "loose-envify": "^1.0.0"
} }
@ -7882,9 +7924,9 @@
} }
}, },
"react-context-toolbox": { "react-context-toolbox": {
"version": "1.2.3", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/react-context-toolbox/-/react-context-toolbox-1.2.3.tgz", "resolved": "https://registry.npmjs.org/react-context-toolbox/-/react-context-toolbox-2.0.2.tgz",
"integrity": "sha512-ArHw0UFDM6X8Z9lHZ1rZOhMcn8TXWC9y9sFpeJm11YTIlQsN4A0MadKcps2pCGMwWKmH0o/67t+TmVapwWm5Sw==" "integrity": "sha512-tY4j0imkYC3n5ZlYSgFkaw7fmlCp3IoQQ6DxpqeNHzcD0hf+6V+/HeJxviLUZ1Rv1Yn3N3xyO2EhkkZwHn0m1A=="
}, },
"react-cookie": { "react-cookie": {
"version": "2.1.6", "version": "2.1.6",
@ -8001,9 +8043,9 @@
} }
}, },
"react-is": { "react-is": {
"version": "16.4.1", "version": "16.8.4",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.4.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.4.tgz",
"integrity": "sha512-xpb0PpALlFWNw/q13A+1aHeyJyLYCg0/cCHPUA43zYluZuIPHaHL3k8OBsTgQtxqW0FhyDEMvi8fZ/+7+r4OSQ==" "integrity": "sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA=="
}, },
"react-lifecycles-compat": { "react-lifecycles-compat": {
"version": "3.0.4", "version": "3.0.4",
@ -8038,9 +8080,9 @@
"integrity": "sha512-IBivBP7xayM7SbbVlAnKgHgoWdfCVqnNBNgQRY5x9iFQm55tFdolR02hX1fCJJtTEKnbaL1stB72/TZc6+p2+Q==" "integrity": "sha512-IBivBP7xayM7SbbVlAnKgHgoWdfCVqnNBNgQRY5x9iFQm55tFdolR02hX1fCJJtTEKnbaL1stB72/TZc6+p2+Q=="
}, },
"react-overlays": { "react-overlays": {
"version": "1.0.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-1.0.0.tgz", "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-1.2.0.tgz",
"integrity": "sha512-YDuUwqWBuVvQvvxPTxKpCuaEZRegDhfgJdQUbVmWVI4gag53zukN/6tNxt0XZpgODQVzLf/w7dFuoDq7YMhygg==", "integrity": "sha512-i/FCV8wR6aRaI+Kz/dpJhOdyx+ah2tN1RhT9InPrexyC4uzf3N4bNayFTGtUeQVacj57j1Mqh1CwV60/5153Iw==",
"requires": { "requires": {
"classnames": "^2.2.6", "classnames": "^2.2.6",
"dom-helpers": "^3.4.0", "dom-helpers": "^3.4.0",
@ -8048,13 +8090,14 @@
"prop-types-extra": "^1.1.0", "prop-types-extra": "^1.1.0",
"react-context-toolbox": "^2.0.2", "react-context-toolbox": "^2.0.2",
"react-popper": "^1.3.2", "react-popper": "^1.3.2",
"uncontrollable": "^6.0.0",
"warning": "^4.0.2" "warning": "^4.0.2"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": { "@babel/runtime": {
"version": "7.2.0", "version": "7.3.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.2.0.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.4.tgz",
"integrity": "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg==", "integrity": "sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g==",
"requires": { "requires": {
"regenerator-runtime": "^0.12.0" "regenerator-runtime": "^0.12.0"
} }
@ -8072,19 +8115,23 @@
"@babel/runtime": "^7.1.2" "@babel/runtime": "^7.1.2"
} }
}, },
"prop-types": { "loose-envify": {
"version": "15.6.2", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"requires": { "requires": {
"loose-envify": "^1.3.1", "js-tokens": "^3.0.0 || ^4.0.0"
"object-assign": "^4.1.1"
} }
}, },
"react-context-toolbox": { "prop-types": {
"version": "2.0.2", "version": "15.7.2",
"resolved": "https://registry.npmjs.org/react-context-toolbox/-/react-context-toolbox-2.0.2.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
"integrity": "sha512-tY4j0imkYC3n5ZlYSgFkaw7fmlCp3IoQQ6DxpqeNHzcD0hf+6V+/HeJxviLUZ1Rv1Yn3N3xyO2EhkkZwHn0m1A==" "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
"requires": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.8.1"
}
}, },
"regenerator-runtime": { "regenerator-runtime": {
"version": "0.12.1", "version": "0.12.1",
@ -8092,9 +8139,9 @@
"integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==" "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg=="
}, },
"warning": { "warning": {
"version": "4.0.2", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.2.tgz", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-wbTp09q/9C+jJn4KKJfJfoS6VleK/Dti0yqWSm6KMvJ4MRCXFQNapHuJXutJIrWV0Cf4AhTdeIe4qdKHR1+Hug==", "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"requires": { "requires": {
"loose-envify": "^1.0.0" "loose-envify": "^1.0.0"
} }
@ -8111,9 +8158,9 @@
} }
}, },
"react-popper": { "react-popper": {
"version": "1.3.2", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.2.tgz", "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.3.tgz",
"integrity": "sha512-UbFWj55Yt9uqvy0oZ+vULDL2Bw1oxeZF9/JzGyxQ5ypgauRH/XlarA5+HLZWro/Zss6Ht2kqpegtb6sYL8GUGw==", "integrity": "sha512-ynMZBPkXONPc5K4P5yFWgZx5JGAUIP3pGGLNs58cfAPgK67olx7fmLp+AdpZ0+GoQ+ieFDa/z4cdV6u7sioH6w==",
"requires": { "requires": {
"@babel/runtime": "^7.1.2", "@babel/runtime": "^7.1.2",
"create-react-context": "<=0.2.2", "create-react-context": "<=0.2.2",
@ -8124,9 +8171,9 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime": { "@babel/runtime": {
"version": "7.2.0", "version": "7.3.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.2.0.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.4.tgz",
"integrity": "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg==", "integrity": "sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g==",
"requires": { "requires": {
"regenerator-runtime": "^0.12.0" "regenerator-runtime": "^0.12.0"
} }
@ -8140,13 +8187,22 @@
"gud": "^1.0.0" "gud": "^1.0.0"
} }
}, },
"prop-types": { "loose-envify": {
"version": "15.6.2", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"requires": { "requires": {
"loose-envify": "^1.3.1", "js-tokens": "^3.0.0 || ^4.0.0"
"object-assign": "^4.1.1" }
},
"prop-types": {
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
"requires": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.8.1"
} }
}, },
"regenerator-runtime": { "regenerator-runtime": {
@ -8160,9 +8216,9 @@
"integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==" "integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q=="
}, },
"warning": { "warning": {
"version": "4.0.2", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.2.tgz", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-wbTp09q/9C+jJn4KKJfJfoS6VleK/Dti0yqWSm6KMvJ4MRCXFQNapHuJXutJIrWV0Cf4AhTdeIe4qdKHR1+Hug==", "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"requires": { "requires": {
"loose-envify": "^1.0.0" "loose-envify": "^1.0.0"
} }
@ -8202,27 +8258,6 @@
"prop-types": "^15.5.10" "prop-types": "^15.5.10"
} }
}, },
"react-router": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-3.2.0.tgz",
"integrity": "sha512-sXlLOg0TRCqnjCVskqBHGjzNjcJKUqXEKnDSuxMYJSPJNq9hROE9VsiIW2kfIq7Ev+20Iz0nxayekXyv0XNmsg==",
"requires": {
"create-react-class": "^15.5.1",
"history": "^3.0.0",
"hoist-non-react-statics": "^1.2.0",
"invariant": "^2.2.1",
"loose-envify": "^1.2.0",
"prop-types": "^15.5.6",
"warning": "^3.0.0"
},
"dependencies": {
"hoist-non-react-statics": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz",
"integrity": "sha1-qkSM8JhtVcxAdzsXF0t90GbLfPs="
}
}
},
"react-router-bootstrap": { "react-router-bootstrap": {
"version": "0.24.4", "version": "0.24.4",
"resolved": "https://registry.npmjs.org/react-router-bootstrap/-/react-router-bootstrap-0.24.4.tgz", "resolved": "https://registry.npmjs.org/react-router-bootstrap/-/react-router-bootstrap-0.24.4.tgz",
@ -9569,9 +9604,9 @@
"optional": true "optional": true
}, },
"uncontrollable": { "uncontrollable": {
"version": "6.0.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-6.0.0.tgz", "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-6.1.0.tgz",
"integrity": "sha512-gmy2ESW40LGbijSbW5piBGiPv55IgyDbjQcMr7LkDR5icpw/06UgMqULAGDBAcFn2a9d/SRPgcb3oo8hdEUfIw==", "integrity": "sha512-2TzEm0pLKauMBZfAZXsgQvLpZHEp95891frCZdGDrSG7dWYaIQhedwLAzi0X8pR8KHNqlmuYEb2cEgbQzr050A==",
"requires": { "requires": {
"invariant": "^2.2.4" "invariant": "^2.2.4"
}, },
@ -9717,6 +9752,21 @@
"os-homedir": "^1.0.0" "os-homedir": "^1.0.0"
} }
}, },
"util": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
"requires": {
"inherits": "2.0.1"
},
"dependencies": {
"inherits": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
"integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE="
}
}
},
"util-deprecate": { "util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -9852,7 +9902,7 @@
}, },
"chalk": { "chalk": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"requires": { "requires": {
"ansi-styles": "^2.2.1", "ansi-styles": "^2.2.1",

View file

@ -1,6 +1,6 @@
{ {
"name": "Vulcan", "name": "Vulcan",
"version": "1.12.17", "version": "1.13.0",
"engines": { "engines": {
"npm": "^3.0" "npm": "^3.0"
}, },
@ -21,7 +21,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "7.0.0-beta.55", "@babel/runtime": "7.1.2",
"analytics-node": "^2.1.1", "analytics-node": "^2.1.1",
"apollo-cache-inmemory": "^1.4.2", "apollo-cache-inmemory": "^1.4.2",
"apollo-client": "2.4.12", "apollo-client": "2.4.12",
@ -30,6 +30,7 @@
"apollo-link-error": "^1.1.5", "apollo-link-error": "^1.1.5",
"apollo-link-schema": "^1.1.1", "apollo-link-schema": "^1.1.1",
"apollo-link-state": "^0.4.2", "apollo-link-state": "^0.4.2",
"apollo-link-watched-mutation": "^0.1.0",
"apollo-server": "2.3.3", "apollo-server": "2.3.3",
"apollo-server-express": "2.3.3", "apollo-server-express": "2.3.3",
"babel-runtime": "^6.26.0", "babel-runtime": "^6.26.0",
@ -70,14 +71,14 @@
"moment": "^2.13.0", "moment": "^2.13.0",
"prop-types": "^15.6.0", "prop-types": "^15.6.0",
"qs": "^6.6.0", "qs": "^6.6.0",
"react": "^16.2.0", "react": "^16.4.1",
"react-addons-pure-render-mixin": "^15.4.1", "react-addons-pure-render-mixin": "^15.4.1",
"react-apollo": "^2.4.1", "react-apollo": "^2.4.1",
"react-bootstrap": "^1.0.0-beta.3", "react-bootstrap": "^1.0.0-beta.5",
"react-bootstrap-datetimepicker": "0.0.22", "react-bootstrap-datetimepicker": "0.0.22",
"react-cookie": "^2.1.4", "react-cookie": "^2.1.4",
"react-datetime": "^2.11.1", "react-datetime": "^2.11.1",
"react-dom": "^16.2.0", "react-dom": "^16.4.1",
"react-dropzone": "^8.0.3", "react-dropzone": "^8.0.3",
"react-helmet": "^5.1.3", "react-helmet": "^5.1.3",
"react-intl": "^2.1.3", "react-intl": "^2.1.3",

View file

@ -6,7 +6,7 @@ import { STATES } from '../../helpers.js';
class AccountsEnrollAccount extends PureComponent { class AccountsEnrollAccount extends PureComponent {
componentDidMount() { componentDidMount() {
const token = this.props.params.token; const token = this.props.match.params.token;
Accounts._loginButtonsSession.set('enrollAccountToken', token); Accounts._loginButtonsSession.set('enrollAccountToken', token);
} }

View file

@ -265,9 +265,20 @@ export class AccountsLoginFormInner extends TrackerComponent {
} }
fields() { fields() {
const loginFields = []; let loginFields = [];
const { formState } = this.state; const { formState } = this.state;
// if extra fields have been specified, add onChange handler to them
if (this.props.extraFields) {
loginFields = this.props.extraFields.map(field => {
const { id } = field;
return {
...field,
onChange: this.handleChange.bind(this, id),
}
});
}
if (!hasPasswordService() && getLoginServices().length == 0) { if (!hasPasswordService() && getLoginServices().length == 0) {
loginFields.push({ loginFields.push({
label: 'No login service added, i.e. accounts-password', label: 'No login service added, i.e. accounts-password',
@ -766,6 +777,13 @@ export class AccountsLoginFormInner extends TrackerComponent {
onSubmitHook onSubmitHook
} = this.state; } = this.state;
// add extra fields to options
if (this.props.extraFields) {
this.props.extraFields.forEach(({ id })=> {
options[id] = this.state[id];
});
}
const self = this; const self = this;
let error = false; let error = false;

View file

@ -1,6 +1,6 @@
Package.describe({ Package.describe({
name: 'vulcan:accounts', name: 'vulcan:accounts',
version: '1.12.17', version: '1.13.0',
summary: 'Accounts UI for React in Meteor 1.3+', summary: 'Accounts UI for React in Meteor 1.3+',
git: 'https://github.com/studiointeract/accounts-ui', git: 'https://github.com/studiointeract/accounts-ui',
documentation: 'README.md', documentation: 'README.md',
@ -9,7 +9,7 @@ Package.describe({
Package.onUse(function(api) { Package.onUse(function(api) {
api.versionsFrom('1.6.1'); api.versionsFrom('1.6.1');
api.use('vulcan:core@1.12.17'); api.use('vulcan:core@1.13.0');
api.use('ecmascript'); api.use('ecmascript');
api.use('tracker'); api.use('tracker');

View file

@ -1,7 +1,7 @@
Package.describe({ Package.describe({
name: 'vulcan:admin', name: 'vulcan:admin',
summary: 'Vulcan components package', summary: 'Vulcan components package',
version: '1.12.17', version: '1.13.0',
git: 'https://github.com/VulcanJS/Vulcan.git', git: 'https://github.com/VulcanJS/Vulcan.git',
}); });
@ -12,7 +12,7 @@ Package.onUse(function(api) {
'fourseven:scss@4.10.0', 'fourseven:scss@4.10.0',
'dynamic-import@0.1.1', 'dynamic-import@0.1.1',
// Vulcan packages // Vulcan packages
'vulcan:core@1.12.17', 'vulcan:core@1.13.0',
]); ]);
api.mainModule('lib/server/main.js', 'server'); api.mainModule('lib/server/main.js', 'server');

View file

@ -1,14 +1,14 @@
Package.describe({ Package.describe({
name: 'vulcan:cloudinary', name: 'vulcan:cloudinary',
summary: 'Vulcan file upload package.', summary: 'Vulcan file upload package.',
version: '1.12.17', version: '1.13.0',
git: 'https://github.com/VulcanJS/Vulcan.git', git: 'https://github.com/VulcanJS/Vulcan.git',
}); });
Package.onUse(function(api) { Package.onUse(function(api) {
api.versionsFrom('1.6.1'); api.versionsFrom('1.6.1');
api.use(['vulcan:core@1.12.17']); api.use(['vulcan:core@1.13.0']);
api.mainModule('lib/client/main.js', 'client'); api.mainModule('lib/client/main.js', 'client');
api.mainModule('lib/server/main.js', 'server'); api.mainModule('lib/server/main.js', 'server');

View file

@ -2,7 +2,7 @@ import { Components, registerComponent } from 'meteor/vulcan:lib';
import React from 'react'; import React from 'react';
import { FormattedMessage, intlShape } from 'meteor/vulcan:i18n'; import { FormattedMessage, intlShape } from 'meteor/vulcan:i18n';
const EditButton = ({ style = 'primary', label, size, showId, modalProps, ...props }, { intl }) => ( const EditButton = ({ style = 'primary', label, size, showId, modalProps, formProps, ...props }, { intl }) => (
<Components.ModalTrigger <Components.ModalTrigger
label={label || intl.formatMessage({ id: 'datatable.edit' })} label={label || intl.formatMessage({ id: 'datatable.edit' })}
component={ component={
@ -12,7 +12,7 @@ const EditButton = ({ style = 'primary', label, size, showId, modalProps, ...pro
} }
modalProps={modalProps} modalProps={modalProps}
> >
<Components.EditForm {...props} /> <Components.EditForm {...props} formProps={formProps}/>
</Components.ModalTrigger> </Components.ModalTrigger>
); );
@ -29,7 +29,7 @@ registerComponent('EditButton', EditButton);
EditForm Component EditForm Component
*/ */
const EditForm = ({ closeModal, successCallback, removeSuccessCallback, ...props }) => { const EditForm = ({ closeModal, successCallback, removeSuccessCallback, formProps, ...props }) => {
const success = successCallback const success = successCallback
? document => { ? document => {
@ -46,7 +46,7 @@ const EditForm = ({ closeModal, successCallback, removeSuccessCallback, ...props
: closeModal; : closeModal;
return ( return (
<Components.SmartForm successCallback={success} removeSuccessCallback={remove} {...props} /> <Components.SmartForm successCallback={success} removeSuccessCallback={remove} {...formProps} {...props} />
); );
}; };
registerComponent('EditForm', EditForm); registerComponent('EditForm', EditForm);

View file

@ -2,7 +2,7 @@ import { Components, registerComponent } from 'meteor/vulcan:lib';
import React from 'react'; import React from 'react';
import { FormattedMessage, intlShape } from 'meteor/vulcan:i18n'; import { FormattedMessage, intlShape } from 'meteor/vulcan:i18n';
const NewButton = ({ collection, size, label, style = 'primary', ...props }, { intl }) => ( const NewButton = ({ collection, size, label, style = 'primary', formProps, ...props }, { intl }) => (
<Components.ModalTrigger <Components.ModalTrigger
label={label || intl.formatMessage({ id: 'datatable.new' })} label={label || intl.formatMessage({ id: 'datatable.new' })}
component={ component={
@ -11,7 +11,7 @@ const NewButton = ({ collection, size, label, style = 'primary', ...props }, { i
</Components.Button> </Components.Button>
} }
> >
<Components.NewForm collection={collection} {...props} /> <Components.NewForm collection={collection} formProps={formProps} {...props} />
</Components.ModalTrigger> </Components.ModalTrigger>
); );
@ -28,7 +28,7 @@ registerComponent('NewButton', NewButton);
NewForm Component NewForm Component
*/ */
const NewForm = ({ closeModal, successCallback, ...props }) => { const NewForm = ({ closeModal, successCallback, formProps, ...props }) => {
const success = successCallback const success = successCallback
? document => { ? document => {
@ -37,6 +37,6 @@ const NewForm = ({ closeModal, successCallback, ...props }) => {
} }
: closeModal; : closeModal;
return <Components.SmartForm successCallback={success} {...props} />; return <Components.SmartForm successCallback={success} {...formProps} {...props} />;
}; };
registerComponent('NewForm', NewForm); registerComponent('NewForm', NewForm);

View file

@ -28,6 +28,135 @@ import get from 'lodash/get';
const defaultOptions = { create: true, update: true, upsert: true, delete: true }; const defaultOptions = { create: true, update: true, upsert: true, delete: true };
/**
* Safe getter
* Must returns null if the document is absent (eg in case of validation failure)
* @param {*} mutation
* @param {*} mutationName
*/
const getDocumentFromMutation = (mutation, mutationName) => {
const mutationData = (mutation.result.data[mutationName] || {});
const document = mutationData.data;
return document;
};
const getCreateMutationName = (typeName) => `create${typeName}`;
const getUpdateMutationName = (typeName) => `update${typeName}`;
const getDeleteMutationName = (typeName) => `delete${typeName}`;
const getUpsertMutationName = (typeName) => `upsert${typeName}`;
const getMultiResolverName = (typeName) => Utils.camelCaseify(Utils.pluralize(typeName));
const getMultiQueryName = (typeName) => `multi${typeName}Query`;
/*
Handle post-mutation updates of the client cache
TODO: this is a client only function
it should be called by a callback on collection create?
*/
export const registerWatchedMutations = (mutations, typeName) => {
if (Meteor.isClient) {
const multiQueryName = getMultiQueryName(typeName);
const multiResolverName = getMultiResolverName(typeName);
// create
if (mutations.create) {
const mutationName = mutations.create.name;
registerWatchedMutation(mutationName, multiQueryName, ({ mutation, query }) => {
// get mongo selector and options objects based on current terms
const terms = query.variables.input.terms;
const collection = Collections.find(c => c.typeName === typeName);
const parameters = collection.getParameters(terms /* apolloClient */);
const { selector, options } = parameters;
let results = query.result;
const document = getDocumentFromMutation(mutation, mutationName);
// nothing to add
if (!document) return results;
if (belongsToSet(document, selector)) {
if (!isInSet(results[multiResolverName], document)) {
// make sure document hasn't been already added as this may be called several times
results[multiResolverName] = addToSet(results[multiResolverName], document);
}
results[multiResolverName] = reorderSet(results[multiResolverName], options.sort);
}
results[multiResolverName].__typename = `Multi${typeName}Output`;
// console.log('// create');
// console.log(mutation);
// console.log(query);
// console.log(collection);
// console.log(parameters);
// console.log(results);
return results;
});
}
//update
if (mutations.update) {
const mutationName = mutations.update.name;
registerWatchedMutation(mutationName, multiQueryName, ({ mutation, query }) => {
// get mongo selector and options objects based on current terms
const terms = query.variables.input.terms;
const collection = Collections.find(c => c.typeName === typeName);
const parameters = collection.getParameters(terms /* apolloClient */);
const { selector, options } = parameters;
let results = query.result;
const document = getDocumentFromMutation(mutation, mutationName);
// nothing to update
if (!document) return results;
if (belongsToSet(document, selector)) {
// edited document belongs to the list
if (!isInSet(results[multiResolverName], document)) {
// if document wasn't already in list, add it
results[multiResolverName] = addToSet(results[multiResolverName], document);
} else {
// if document was already in the list, update it
results[multiResolverName] = updateInSet(results[multiResolverName], document);
}
results[multiResolverName] = reorderSet(
results[multiResolverName],
options.sort,
selector
);
} else {
// if edited doesn't belong to current list anymore (based on view selector), remove it
results[multiResolverName] = removeFromSet(results[multiResolverName], document);
}
results[multiResolverName].__typename = `Multi${typeName}Output`;
// console.log('// update');
// console.log(mutation);
// console.log(query);
// console.log(parameters);
// console.log(results);
return results;
});
}
//delete
if (mutations.delete) {
const mutationName = mutations.delete.name;
registerWatchedMutation(mutationName, multiQueryName, ({ mutation, query }) => {
let results = query.result;
const document = getDocumentFromMutation(mutation, mutationName);
// nothing to delete
if (!document) return results;
results[multiResolverName] = removeFromSet(results[multiResolverName], document);
results[multiResolverName].__typename = `Multi${typeName}Output`;
// console.log('// delete')
// console.log(mutation);
// console.log(query);
// console.log(parameters);
// console.log(results);
return results;
});
}
}
};
export function getDefaultMutations(options) { export function getDefaultMutations(options) {
let typeName, collectionName, mutationOptions; let typeName, collectionName, mutationOptions;
@ -46,17 +175,16 @@ export function getDefaultMutations(options) {
// register callbacks for documentation purposes // register callbacks for documentation purposes
registerCollectionCallbacks(typeName, mutationOptions); registerCollectionCallbacks(typeName, mutationOptions);
const multiResolverName = Utils.camelCaseify(Utils.pluralize(typeName));
const multiQueryName = `multi${typeName}Query`;
const mutations = {}; const mutations = {};
if (mutationOptions.create) { if (mutationOptions.create) {
// mutation for inserting a new document // mutation for inserting a new document
const mutationName = `create${typeName}`; const mutationName = getCreateMutationName(typeName);
const createMutation = { const createMutation = {
description: `Mutation for creating new ${typeName} documents`, description: `Mutation for creating new ${typeName} documents`,
name: mutationName,
// check function called on a user to see if they can perform the operation // check function called on a user to see if they can perform the operation
check(user, document) { check(user, document) {
@ -99,51 +227,16 @@ export function getDefaultMutations(options) {
mutations.create = createMutation; mutations.create = createMutation;
// OpenCRUD backwards compatibility // OpenCRUD backwards compatibility
mutations.new = createMutation; mutations.new = createMutation;
/*
Handle post-mutation updates of the client cache
*/
if (Meteor.isClient) {
registerWatchedMutation(mutationName, multiQueryName, ({ mutation, query }) => {
// get mongo selector and options objects based on current terms
const terms = query.variables.input.terms;
const collection = Collections.find(c => c.typeName === typeName);
const parameters = collection.getParameters(terms /* apolloClient */);
const { selector, options } = parameters;
let results = query.result;
const document = get(mutation, `result.data['${mutationName}'.data]`, {});
if (belongsToSet(document, selector)) {
if (!isInSet(results[multiResolverName], document)) {
// make sure document hasn't been already added as this may be called several times
results[multiResolverName] = addToSet(results[multiResolverName], document);
}
results[multiResolverName] = reorderSet(results[multiResolverName], options.sort);
}
results[multiResolverName].__typename = `Multi${typeName}Output`;
// console.log('// create');
// console.log(mutation);
// console.log(query);
// console.log(collection);
// console.log(parameters);
// console.log(results);
return results;
});
}
} }
if (mutationOptions.update) { if (mutationOptions.update) {
// mutation for editing a specific document // mutation for editing a specific document
const mutationName = `update${typeName}`; const mutationName = getUpdateMutationName(typeName);
const updateMutation = { const updateMutation = {
description: `Mutation for updating a ${typeName} document`, description: `Mutation for updating a ${typeName} document`,
name: mutationName,
// check function called on a user and document to see if they can perform the operation // check function called on a user and document to see if they can perform the operation
check(user, document) { check(user, document) {
@ -160,13 +253,13 @@ export function getDefaultMutations(options) {
// OpenCRUD backwards compatibility // OpenCRUD backwards compatibility
return Users.owns(user, document) return Users.owns(user, document)
? Users.canDo(user, [ ? Users.canDo(user, [
`${typeName.toLowerCase()}.update.own`, `${typeName.toLowerCase()}.update.own`,
`${collectionName.toLowerCase()}.edit.own`, `${collectionName.toLowerCase()}.edit.own`,
]) ])
: Users.canDo(user, [ : Users.canDo(user, [
`${typeName.toLowerCase()}.update.all`, `${typeName.toLowerCase()}.update.all`,
`${collectionName.toLowerCase()}.edit.all`, `${collectionName.toLowerCase()}.edit.all`,
]); ]);
}, },
async mutation(root, { selector, data }, context) { async mutation(root, { selector, data }, context) {
@ -212,56 +305,13 @@ export function getDefaultMutations(options) {
// OpenCRUD backwards compatibility // OpenCRUD backwards compatibility
mutations.edit = updateMutation; mutations.edit = updateMutation;
/*
Handle post-mutation updates of the client cache
*/
if (Meteor.isClient) {
registerWatchedMutation(mutationName, multiQueryName, ({ mutation, query }) => {
// get mongo selector and options objects based on current terms
const terms = query.variables.input.terms;
const collection = Collections.find(c => c.typeName === typeName);
const parameters = collection.getParameters(terms /* apolloClient */);
const { selector, options } = parameters;
let results = query.result;
const document = get(mutation, `result.data['${mutationName}'.data]`, {});
if (belongsToSet(document, selector)) {
// edited document belongs to the list
if (!isInSet(results[multiResolverName], document)) {
// if document wasn't already in list, add it
results[multiResolverName] = addToSet(results[multiResolverName], document);
} else {
// if document was already in the list, update it
results[multiResolverName] = updateInSet(results[multiResolverName], document);
}
results[multiResolverName] = reorderSet(
results[multiResolverName],
options.sort,
selector
);
} else {
// if edited doesn't belong to current list anymore (based on view selector), remove it
results[multiResolverName] = removeFromSet(results[multiResolverName], document);
}
results[multiResolverName].__typename = `Multi${typeName}Output`;
// console.log('// update');
// console.log(mutation);
// console.log(query);
// console.log(parameters);
// console.log(results);
return results;
});
}
} }
if (mutationOptions.upsert) { if (mutationOptions.upsert) {
// mutation for upserting a specific document // mutation for upserting a specific document
const mutationName = getUpsertMutationName(typeName);
mutations.upsert = { mutations.upsert = {
description: `Mutation for upserting a ${typeName} document`, description: `Mutation for upserting a ${typeName} document`,
name: mutationName,
async mutation(root, { selector, data }, context) { async mutation(root, { selector, data }, context) {
const collection = context[collectionName]; const collection = context[collectionName];
@ -286,10 +336,11 @@ export function getDefaultMutations(options) {
if (mutationOptions.delete) { if (mutationOptions.delete) {
// mutation for removing a specific document (same checks as edit mutation) // mutation for removing a specific document (same checks as edit mutation)
const mutationName = `delete${typeName}`; const mutationName = getDeleteMutationName(typeName);
const deleteMutation = { const deleteMutation = {
description: `Mutation for deleting a ${typeName} document`, description: `Mutation for deleting a ${typeName} document`,
name: mutationName,
check(user, document) { check(user, document) {
// OpenCRUD backwards compatibility // OpenCRUD backwards compatibility
@ -302,13 +353,13 @@ export function getDefaultMutations(options) {
// OpenCRUD backwards compatibility // OpenCRUD backwards compatibility
return Users.owns(user, document) return Users.owns(user, document)
? Users.canDo(user, [ ? Users.canDo(user, [
`${typeName.toLowerCase()}.delete.own`, `${typeName.toLowerCase()}.delete.own`,
`${collectionName.toLowerCase()}.remove.own`, `${collectionName.toLowerCase()}.remove.own`,
]) ])
: Users.canDo(user, [ : Users.canDo(user, [
`${typeName.toLowerCase()}.delete.all`, `${typeName.toLowerCase()}.delete.all`,
`${collectionName.toLowerCase()}.remove.all`, `${collectionName.toLowerCase()}.remove.all`,
]); ]);
}, },
async mutation(root, { selector }, context) { async mutation(root, { selector }, context) {
@ -351,26 +402,10 @@ export function getDefaultMutations(options) {
// OpenCRUD backwards compatibility // OpenCRUD backwards compatibility
mutations.remove = deleteMutation; mutations.remove = deleteMutation;
/* }
Handle post-mutation updates of the client cache if (Meteor.isClient) {
registerWatchedMutations(mutations, typeName);
*/
if (Meteor.isClient) {
registerWatchedMutation(mutationName, multiQueryName, ({ mutation, query }) => {
let results = query.result;
const document = get(mutation, `result.data['${mutationName}'.data]`, {});
results[multiResolverName] = removeFromSet(results[multiResolverName], document);
results[multiResolverName].__typename = `Multi${typeName}Output`;
// console.log('// delete')
// console.log(mutation);
// console.log(query);
// console.log(parameters);
// console.log(results);
return results;
});
}
} }
return mutations; return mutations;

View file

@ -1,7 +1,7 @@
Package.describe({ Package.describe({
name: 'vulcan:core', name: 'vulcan:core',
summary: 'Vulcan core package', summary: 'Vulcan core package',
version: '1.12.17', version: '1.13.0',
git: 'https://github.com/VulcanJS/Vulcan.git', git: 'https://github.com/VulcanJS/Vulcan.git',
}); });
@ -9,14 +9,14 @@ Package.onUse(function(api) {
api.versionsFrom('1.6.1'); api.versionsFrom('1.6.1');
api.use([ api.use([
'vulcan:lib@1.12.17', 'vulcan:lib@1.13.0',
'vulcan:i18n@1.12.17', 'vulcan:i18n@1.13.0',
'vulcan:users@1.12.17', 'vulcan:users@1.13.0',
'vulcan:routing@1.12.17', 'vulcan:routing@1.13.0',
'vulcan:debug@1.12.17', 'vulcan:debug@1.13.0',
]); ]);
api.imply(['vulcan:lib@1.12.17']); api.imply(['vulcan:lib@1.13.0']);
api.mainModule('lib/server/main.js', 'server'); api.mainModule('lib/server/main.js', 'server');
api.mainModule('lib/client/main.js', 'client'); api.mainModule('lib/client/main.js', 'client');
@ -25,4 +25,5 @@ Package.onUse(function(api) {
Package.onTest(function(api) { Package.onTest(function(api) {
api.use(['ecmascript', 'meteortesting:mocha', 'vulcan:test', 'vulcan:core']); api.use(['ecmascript', 'meteortesting:mocha', 'vulcan:test', 'vulcan:core']);
api.mainModule('./test/index.js'); api.mainModule('./test/index.js');
api.mainModule('./test/client/index.js', ['client']);
}); });

View file

@ -0,0 +1 @@
import './mutations.test';

View file

@ -0,0 +1,19 @@
import { registerWatchedMutations } from '../../lib/modules/default_mutations';
import expect from 'expect';
describe('vulcan:core/registerWatchedMutations', function(){
it('should registerWatchedMutations without failing', function(){
registerWatchedMutations({
create:{
name:'createFoo'
},
update:{
name:'updateFoo'
},
delete:{
name: 'deleteFoo'
}
}, 'Foo');
expect(true).toBe(true);
});
});

View file

@ -1,3 +1,4 @@
import './mutations.test';
import './resolvers.test'; import './resolvers.test';
import './components.test'; import './components.test';
import './containers.test'; import './containers.test';

View file

@ -0,0 +1,29 @@
import { getDefaultMutations } from '../lib/modules/default_mutations';
import expect from 'expect';
describe('vulcan:core/default_mutations', function() {
it('returns mutations', function(){
const mutations = getDefaultMutations({
typeName:'Foo',
collectionName:'Foos',
options: {}
});
expect(mutations.create).toBeDefined();
expect(mutations.update).toBeDefined();
expect(mutations.delete).toBeDefined();
});
it('preserves openCRUD backward compatibility', function(){
const mutations = getDefaultMutations({
typeName:'Foo',
collectionName:'Foos',
options: {}
});
expect(mutations.new).toBeDefined();
expect(mutations.edit).toBeDefined();
expect(mutations.remove).toBeDefined();
});
});

View file

@ -1,7 +1,7 @@
Package.describe({ Package.describe({
name: 'vulcan:debug', name: 'vulcan:debug',
summary: 'Vulcan debug package', summary: 'Vulcan debug package',
version: '1.12.17', version: '1.13.0',
git: 'https://github.com/VulcanJS/Vulcan.git', git: 'https://github.com/VulcanJS/Vulcan.git',
debugOnly: true, debugOnly: true,
}); });
@ -15,8 +15,8 @@ Package.onUse(function(api) {
// Vulcan packages // Vulcan packages
'vulcan:lib@1.12.17', 'vulcan:lib@1.13.0',
'vulcan:email@1.12.17', 'vulcan:email@1.13.0',
]); ]);
api.addFiles(['lib/stylesheets/debug.scss'], ['client']); api.addFiles(['lib/stylesheets/debug.scss'], ['client']);

View file

@ -1,14 +1,14 @@
Package.describe({ Package.describe({
name: 'vulcan:email', name: 'vulcan:email',
summary: 'Vulcan email package', summary: 'Vulcan email package',
version: '1.12.17', version: '1.13.0',
git: 'https://github.com/VulcanJS/Vulcan.git', git: 'https://github.com/VulcanJS/Vulcan.git',
}); });
Package.onUse(function(api) { Package.onUse(function(api) {
api.versionsFrom('1.6.1'); api.versionsFrom('1.6.1');
api.use(['vulcan:lib@1.12.17']); api.use(['vulcan:lib@1.13.0']);
api.mainModule('lib/server.js', 'server'); api.mainModule('lib/server.js', 'server');
api.mainModule('lib/client.js', 'client'); api.mainModule('lib/client.js', 'client');

View file

@ -1,14 +1,14 @@
Package.describe({ Package.describe({
name: 'vulcan:embed', name: 'vulcan:embed',
summary: 'Vulcan Embed package', summary: 'Vulcan Embed package',
version: '1.12.17', version: '1.13.0',
git: 'https://github.com/VulcanJS/Vulcan.git', git: 'https://github.com/VulcanJS/Vulcan.git',
}); });
Package.onUse(function(api) { Package.onUse(function(api) {
api.versionsFrom('1.6.1'); api.versionsFrom('1.6.1');
api.use(['http', 'vulcan:core@1.12.17', 'fourseven:scss@4.10.0']); api.use(['http', 'vulcan:core@1.13.0', 'fourseven:scss@4.10.0']);
api.addFiles(['lib/stylesheets/embedly.scss'], ['client']); api.addFiles(['lib/stylesheets/embedly.scss'], ['client']);

View file

@ -1,14 +1,14 @@
Package.describe({ Package.describe({
name: 'vulcan:errors-sentry', name: 'vulcan:errors-sentry',
summary: 'Vulcan Sentry error tracking package', summary: 'Vulcan Sentry error tracking package',
version: '1.12.17', version: '1.13.0',
git: 'https://github.com/VulcanJS/Vulcan.git', git: 'https://github.com/VulcanJS/Vulcan.git',
}); });
Package.onUse(function(api) { Package.onUse(function(api) {
api.versionsFrom('1.6.1'); api.versionsFrom('1.6.1');
api.use(['ecmascript', 'vulcan:core@1.12.17', 'vulcan:users@1.12.17', 'vulcan:errors@1.12.17']); api.use(['ecmascript', 'vulcan:core@1.13.0', 'vulcan:users@1.13.0', 'vulcan:errors@1.13.0']);
api.mainModule('lib/server/main.js', 'server'); api.mainModule('lib/server/main.js', 'server');
api.mainModule('lib/client/main.js', 'client'); api.mainModule('lib/client/main.js', 'client');

View file

@ -1,14 +1,14 @@
Package.describe({ Package.describe({
name: 'vulcan:errors', name: 'vulcan:errors',
summary: 'Vulcan error tracking package', summary: 'Vulcan error tracking package',
version: '1.12.17', version: '1.13.0',
git: 'https://github.com/VulcanJS/Vulcan.git', git: 'https://github.com/VulcanJS/Vulcan.git',
}); });
Package.onUse(function(api) { Package.onUse(function(api) {
api.versionsFrom('1.6.1'); api.versionsFrom('1.6.1');
api.use(['ecmascript', 'vulcan:core@1.12.17']); api.use(['ecmascript', 'vulcan:core@1.13.0']);
api.mainModule('lib/server/main.js', 'server'); api.mainModule('lib/server/main.js', 'server');
api.mainModule('lib/client/main.js', 'client'); api.mainModule('lib/client/main.js', 'client');

View file

@ -1,14 +1,14 @@
Package.describe({ Package.describe({
name: 'vulcan:events-ga', name: 'vulcan:events-ga',
summary: 'Vulcan Google Analytics event tracking package', summary: 'Vulcan Google Analytics event tracking package',
version: '1.12.17', version: '1.13.0',
git: 'https://github.com/VulcanJS/Vulcan.git', git: 'https://github.com/VulcanJS/Vulcan.git',
}); });
Package.onUse(function(api) { Package.onUse(function(api) {
api.versionsFrom('1.6.1'); api.versionsFrom('1.6.1');
api.use(['vulcan:core@1.12.17', 'vulcan:events@1.12.17']); api.use(['vulcan:core@1.13.0', 'vulcan:events@1.13.0']);
api.mainModule('lib/server/main.js', 'server'); api.mainModule('lib/server/main.js', 'server');
api.mainModule('lib/client/main.js', 'client'); api.mainModule('lib/client/main.js', 'client');

View file

@ -1,14 +1,14 @@
Package.describe({ Package.describe({
name: 'vulcan:events-intercom', name: 'vulcan:events-intercom',
summary: 'Vulcan Intercom integration package.', summary: 'Vulcan Intercom integration package.',
version: '1.12.17', version: '1.13.0',
git: 'https://github.com/VulcanJS/Vulcan.git', git: 'https://github.com/VulcanJS/Vulcan.git',
}); });
Package.onUse(function(api) { Package.onUse(function(api) {
api.versionsFrom('1.6.1'); api.versionsFrom('1.6.1');
api.use(['vulcan:core@1.12.17', 'vulcan:events@1.12.17']); api.use(['vulcan:core@1.13.0', 'vulcan:events@1.13.0']);
api.mainModule('lib/client/main.js', 'client'); api.mainModule('lib/client/main.js', 'client');
api.mainModule('lib/server/main.js', 'server'); api.mainModule('lib/server/main.js', 'server');

View file

@ -1,14 +1,14 @@
Package.describe({ Package.describe({
name: 'vulcan:events-internal', name: 'vulcan:events-internal',
summary: 'Vulcan internal event tracking package', summary: 'Vulcan internal event tracking package',
version: '1.12.17', version: '1.13.0',
git: 'https://github.com/VulcanJS/Vulcan.git', git: 'https://github.com/VulcanJS/Vulcan.git',
}); });
Package.onUse(function(api) { Package.onUse(function(api) {
api.versionsFrom('1.6.1'); api.versionsFrom('1.6.1');
api.use(['vulcan:core@1.12.17', 'vulcan:events@1.12.17']); api.use(['vulcan:core@1.13.0', 'vulcan:events@1.13.0']);
api.mainModule('lib/server/main.js', 'server'); api.mainModule('lib/server/main.js', 'server');
api.mainModule('lib/client/main.js', 'client'); api.mainModule('lib/client/main.js', 'client');

View file

@ -1,14 +1,14 @@
Package.describe({ Package.describe({
name: 'vulcan:events-segment', name: 'vulcan:events-segment',
summary: 'Vulcan Segment', summary: 'Vulcan Segment',
version: '1.12.17', version: '1.13.0',
git: 'https://github.com/VulcanJS/Vulcan.git', git: 'https://github.com/VulcanJS/Vulcan.git',
}); });
Package.onUse(function(api) { Package.onUse(function(api) {
api.versionsFrom('1.6.1'); api.versionsFrom('1.6.1');
api.use(['vulcan:core@1.12.17', 'vulcan:events@1.12.17']); api.use(['vulcan:core@1.13.0', 'vulcan:events@1.13.0']);
api.mainModule('lib/server/main.js', 'server'); api.mainModule('lib/server/main.js', 'server');
api.mainModule('lib/client/main.js', 'client'); api.mainModule('lib/client/main.js', 'client');

View file

@ -1,14 +1,14 @@
Package.describe({ Package.describe({
name: 'vulcan:events', name: 'vulcan:events',
summary: 'Vulcan event tracking package', summary: 'Vulcan event tracking package',
version: '1.12.17', version: '1.13.0',
git: 'https://github.com/VulcanJS/Vulcan.git', git: 'https://github.com/VulcanJS/Vulcan.git',
}); });
Package.onUse(function(api) { Package.onUse(function(api) {
api.versionsFrom('1.6.1'); api.versionsFrom('1.6.1');
api.use(['vulcan:core@1.12.17']); api.use(['vulcan:core@1.13.0']);
api.mainModule('lib/server/main.js', 'server'); api.mainModule('lib/server/main.js', 'server');
api.mainModule('lib/client/main.js', 'client'); api.mainModule('lib/client/main.js', 'client');

View file

@ -1,14 +1,14 @@
Package.describe({ Package.describe({
name: 'vulcan:forms-tags', name: 'vulcan:forms-tags',
summary: 'Vulcan tag input package', summary: 'Vulcan tag input package',
version: '1.12.17', version: '1.13.0',
git: 'https://github.com/VulcanJS/Vulcan.git', git: 'https://github.com/VulcanJS/Vulcan.git',
}); });
Package.onUse(function(api) { Package.onUse(function(api) {
api.versionsFrom('1.6.1'); api.versionsFrom('1.6.1');
api.use(['vulcan:core@1.12.17', 'vulcan:forms@1.12.17']); api.use(['vulcan:core@1.13.0', 'vulcan:forms@1.13.0']);
api.mainModule('lib/export.js', ['client', 'server']); api.mainModule('lib/export.js', ['client', 'server']);
}); });

View file

@ -1,14 +1,14 @@
Package.describe({ Package.describe({
name: 'vulcan:forms-upload', name: 'vulcan:forms-upload',
summary: 'Vulcan package extending vulcan:forms to upload images to Cloudinary from a drop zone.', summary: 'Vulcan package extending vulcan:forms to upload images to Cloudinary from a drop zone.',
version: '1.12.17', version: '1.13.0',
git: 'https://github.com/xavcz/nova-forms-upload.git', git: 'https://github.com/xavcz/nova-forms-upload.git',
}); });
Package.onUse(function(api) { Package.onUse(function(api) {
api.versionsFrom('1.6.1'); api.versionsFrom('1.6.1');
api.use(['vulcan:core@1.12.17', 'vulcan:forms@1.12.17', 'fourseven:scss@4.10.0']); api.use(['vulcan:core@1.13.0', 'vulcan:forms@1.13.0', 'fourseven:scss@4.10.0']);
api.addFiles(['lib/Upload.scss'], 'client'); api.addFiles(['lib/Upload.scss'], 'client');

View file

@ -66,7 +66,7 @@ import { callbackProps } from './propTypes';
// props that should trigger a form reset // props that should trigger a form reset
const RESET_PROPS = [ const RESET_PROPS = [
'collection', 'collectionName', 'typeName', 'document', 'schema', 'currentUser', 'collection', 'collectionName', 'typeName', 'document', 'schema', 'currentUser',
'fields', 'removeFields', 'fields', 'removeFields',
'prefilledProps' // TODO: prefilledProps should be merged instead? 'prefilledProps' // TODO: prefilledProps should be merged instead?
]; ];
@ -104,17 +104,17 @@ const getInitialStateFromProps = nextProps => {
nextProps.prefilledProps, nextProps.prefilledProps,
nextProps.document nextProps.document
); );
//if minCount is specified, go ahead and create empty nested documents //if minCount is specified, go ahead and create empty nested documents
Object.keys(convertedSchema).forEach(key => { Object.keys(convertedSchema).forEach(key => {
let minCount = convertedSchema[key].minCount; let minCount = convertedSchema[key].minCount;
if(minCount) { if (minCount) {
initialDocument[key] = initialDocument[key] || []; initialDocument[key] = initialDocument[key] || [];
while(initialDocument[key].length < minCount) while (initialDocument[key].length < minCount)
initialDocument[key].push({}); initialDocument[key].push({});
} }
}); });
// remove all instances of the `__typename` property from document // remove all instances of the `__typename` property from document
Utils.removeProperty(initialDocument, '__typename'); Utils.removeProperty(initialDocument, '__typename');
@ -154,7 +154,7 @@ class SmartForm extends Component {
}; };
} }
defaultValues = {}; defaultValues = {};
submitFormCallbacks = []; submitFormCallbacks = [];
successFormCallbacks = []; successFormCallbacks = [];
@ -266,7 +266,7 @@ class SmartForm extends Component {
}); });
// run data object through submitForm callbacks // run data object through submitForm callbacks
data = runCallbacks({ callbacks: this.submitFormCallbacks, iterator: data, properties: { form: this }}); data = runCallbacks({ callbacks: this.submitFormCallbacks, iterator: data, properties: { form: this } });
return data; return data;
}; };
@ -737,8 +737,72 @@ class SmartForm extends Component {
/* /*
Warn the user if there are unsaved changes Install a route leave hook to warn the user if there are unsaved changes
*/
componentDidMount = () => {
this.checkRouteChange();
this.checkBrowserClosing();
}
/*
Remove the closing browser check on component unmount
see https://gist.github.com/mknabe/bfcb6db12ef52323954a28655801792d
*/
componentWillUnmount = () => {
if (this.getWarnUnsavedChanges()) {
// unblock route change
if (this.unblock) {
this.unblock();
}
// unblock browser change
window.onbeforeunload = undefined; //undefined instead of null to support IE
}
};
// -------------------- Check on form leaving ----- //
/**
* Check if we must warn user on unsaved change
*/
getWarnUnsavedChanges = () => {
let warnUnsavedChanges = getSetting('forms.warnUnsavedChanges');
if (typeof this.props.warnUnsavedChanges === 'boolean') {
warnUnsavedChanges = this.props.warnUnsavedChanges;
}
return warnUnsavedChanges;
}
// check for route change, prevent form content loss
checkRouteChange = () => {
// @see https://github.com/ReactTraining/react-router/issues/4635#issuecomment-297828995
// @see https://github.com/ReactTraining/history#blocking-transitions
if (this.getWarnUnsavedChanges()) {
this.unblock = this.props.history.block((location, action) => {
// return the message that will pop into a window.confirm alert
// if returns nothing, the message won't appear and the user won't be blocked
return this.handleRouteLeave();
/*
// React-router 3 implementtion
const routes = this.props.router.routes;
const currentRoute = routes[routes.length - 1];
this.props.router.setRouteLeaveHook(currentRoute, this.handleRouteLeave);
*/
});
}
}
// check for browser closing
checkBrowserClosing = () => {
//check for closing the browser with unsaved changes too
window.onbeforeunload = this.handlePageLeave;
}
/*
Check if the user has unsaved changes, returns a message if yes
and nothing if not
*/ */
handleRouteLeave = () => { handleRouteLeave = () => {
if (this.isChanged()) { if (this.isChanged()) {
@ -750,9 +814,13 @@ class SmartForm extends Component {
} }
}; };
//see https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload /**
//the message returned is actually ignored by most browsers and a default message 'Are you sure you want to leave this page? You might have unsaved changes' is displayed. See the Notes section on the mozilla docs above * Same for browser closing
handlePageLeave = event => { *
* see https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload
* the message returned is actually ignored by most browsers and a default message 'Are you sure you want to leave this page? You might have unsaved changes' is displayed. See the Notes section on the mozilla docs above
*/
handlePageLeave = (event) => {
if (this.isChanged()) { if (this.isChanged()) {
const message = this.context.intl.formatMessage({ const message = this.context.intl.formatMessage({
id: 'forms.confirm_discard', id: 'forms.confirm_discard',
@ -765,41 +833,6 @@ class SmartForm extends Component {
return message; return message;
} }
}; };
/*
Install a route leave hook to warn the user if there are unsaved changes
*/
componentDidMount = () => {
let warnUnsavedChanges = getSetting('forms.warnUnsavedChanges');
if (typeof this.props.warnUnsavedChanges === 'boolean') {
warnUnsavedChanges = this.props.warnUnsavedChanges;
}
if (warnUnsavedChanges) {
const routes = this.props.router.routes;
const currentRoute = routes[routes.length - 1];
this.props.router.setRouteLeaveHook(currentRoute, this.handleRouteLeave);
//check for closing the browser with unsaved changes
window.onbeforeunload = this.handlePageLeave;
}
};
/*
Remove the closing browser check on component unmount
see https://gist.github.com/mknabe/bfcb6db12ef52323954a28655801792d
*/
componentWillUnmount = () => {
let warnUnsavedChanges = getSetting('forms.warnUnsavedChanges');
if (typeof this.props.warnUnsavedChanges === 'boolean') {
warnUnsavedChanges = this.props.warnUnsavedChanges;
}
if (warnUnsavedChanges) {
window.onbeforeunload = undefined; //undefined instead of null to support IE
}
};
/* /*
Returns true if there are any differences between the initial document and the current one Returns true if there are any differences between the initial document and the current one
@ -899,7 +932,7 @@ class SmartForm extends Component {
} }
// run document through mutation success callbacks // run document through mutation success callbacks
document = runCallbacks({ callbacks: this.successFormCallbacks, iterator: document, properties: { form: this }}); document = runCallbacks({ callbacks: this.successFormCallbacks, iterator: document, properties: { form: this } });
// run success callback if it exists // run success callback if it exists
if (this.props.successCallback) this.props.successCallback(document, { form: this }); if (this.props.successCallback) this.props.successCallback(document, { form: this });
@ -915,7 +948,7 @@ class SmartForm extends Component {
console.log(error); console.log(error);
// run mutation failure callbacks on error, we do not allow the callbacks to change the error // run mutation failure callbacks on error, we do not allow the callbacks to change the error
runCallbacks({ callbacks: this.failureFormCallbacks, iterator: error, properties: { error, form: this }}); runCallbacks({ callbacks: this.failureFormCallbacks, iterator: error, properties: { error, form: this } });
if (!_.isEmpty(error)) { if (!_.isEmpty(error)) {
// add error to state // add error to state
@ -937,7 +970,7 @@ class SmartForm extends Component {
submitForm = event => { submitForm = event => {
event && event.preventDefault(); event && event.preventDefault();
// if form is disabled (there is already a submit handler running) don't do anything // if form is disabled (there is already a submit handler running) don't do anything
if (this.state.disabled) { if (this.state.disabled) {
return; return;
@ -1003,7 +1036,7 @@ class SmartForm extends Component {
} }
}; };
// --------------------------------------------------------------------- // // --------------------------------------------------------------------- //
// ------------------------- Props to Pass ----------------------------- // // ------------------------- Props to Pass ----------------------------- //
// --------------------------------------------------------------------- // // --------------------------------------------------------------------- //
@ -1045,15 +1078,15 @@ class SmartForm extends Component {
cancelCallback: this.props.cancelCallback, cancelCallback: this.props.cancelCallback,
revertCallback: this.props.revertCallback, revertCallback: this.props.revertCallback,
document: this.getDocument(), document: this.getDocument(),
deleteDocument: deleteDocument:
(this.getFormType() === 'edit' && (this.getFormType() === 'edit' &&
this.props.showRemove && this.props.showRemove &&
this.deleteDocument) || this.deleteDocument) ||
null, null,
collectionName:this.props.collectionName, collectionName: this.props.collectionName,
currentValues:this.state.currentValues, currentValues: this.state.currentValues,
deletedValues:this.state.deletedValues, deletedValues: this.state.deletedValues,
errors:this.state.errors, errors: this.state.errors,
}); });
// --------------------------------------------------------------------- // // --------------------------------------------------------------------- //

View file

@ -0,0 +1,14 @@
import React from 'react';
import { registerComponent } from 'meteor/vulcan:core';
// this component receives a ref, so it must be a class component
class FormElement extends React.Component {
render(){
const { children, ...otherProps } = this.props;
return <form {...otherProps}>{children}</form>;
}
}
registerComponent({
name:'FormElement',
component: FormElement
});

View file

@ -1,4 +1,5 @@
import '../components/FieldErrors.jsx'; import '../components/FieldErrors.jsx';
import '../components/FormElement.jsx';
import '../components/FormErrors.jsx'; import '../components/FormErrors.jsx';
import '../components/FormError.jsx'; import '../components/FormError.jsx';
import '../components/FormComponent.jsx'; import '../components/FormComponent.jsx';

View file

@ -1,14 +1,14 @@
Package.describe({ Package.describe({
name: 'vulcan:forms', name: 'vulcan:forms',
summary: 'Form containers for React', summary: 'Form containers for React',
version: '1.12.17', version: '1.13.0',
git: 'https://github.com/meteor-utilities/react-form-containers.git', git: 'https://github.com/meteor-utilities/react-form-containers.git',
}); });
Package.onUse(function(api) { Package.onUse(function(api) {
api.versionsFrom('1.6.1'); api.versionsFrom('1.6.1');
api.use(['vulcan:core@1.12.17']); api.use(['vulcan:core@1.13.0']);
api.mainModule('lib/client/main.js', ['client']); api.mainModule('lib/client/main.js', ['client']);
api.mainModule('lib/server/main.js', ['server']); api.mainModule('lib/server/main.js', ['server']);

View file

@ -1,14 +1,14 @@
Package.describe({ Package.describe({
name: 'vulcan:i18n-en-us', name: 'vulcan:i18n-en-us',
summary: 'Vulcan i18n package (en_US)', summary: 'Vulcan i18n package (en_US)',
version: '1.12.17', version: '1.13.0',
git: 'https://github.com/VulcanJS/Vulcan.git', git: 'https://github.com/VulcanJS/Vulcan.git',
}); });
Package.onUse(function(api) { Package.onUse(function(api) {
api.versionsFrom('1.6.1'); api.versionsFrom('1.6.1');
api.use(['vulcan:core@1.12.17']); api.use(['vulcan:core@1.13.0']);
api.addFiles(['lib/en_US.js'], ['client', 'server']); api.addFiles(['lib/en_US.js'], ['client', 'server']);
}); });

View file

@ -1,14 +1,14 @@
Package.describe({ Package.describe({
name: 'vulcan:i18n-es-es', name: 'vulcan:i18n-es-es',
summary: 'Vulcan i18n package (es_ES)', summary: 'Vulcan i18n package (es_ES)',
version: '1.12.17', version: '1.13.0',
git: 'https://github.com/VulcanJS/Vulcan.git', git: 'https://github.com/VulcanJS/Vulcan.git',
}); });
Package.onUse(function(api) { Package.onUse(function(api) {
api.versionsFrom('1.6.1'); api.versionsFrom('1.6.1');
api.use(['vulcan:core@1.12.17']); api.use(['vulcan:core@1.13.0']);
api.addFiles(['lib/es_ES.js'], ['client', 'server']); api.addFiles(['lib/es_ES.js'], ['client', 'server']);
}); });

View file

@ -1,14 +1,14 @@
Package.describe({ Package.describe({
name: 'vulcan:i18n-fr-fr', name: 'vulcan:i18n-fr-fr',
summary: 'Vulcan i18n package (fr_FR)', summary: 'Vulcan i18n package (fr_FR)',
version: '1.12.17', version: '1.13.0',
git: 'https://github.com/VulcanJS/Vulcan.git', git: 'https://github.com/VulcanJS/Vulcan.git',
}); });
Package.onUse(function(api) { Package.onUse(function(api) {
api.versionsFrom('1.6.1'); api.versionsFrom('1.6.1');
api.use(['vulcan:core@1.12.17']); api.use(['vulcan:core@1.13.0']);
api.addFiles(['lib/fr_FR.js'], ['client', 'server']); api.addFiles(['lib/fr_FR.js'], ['client', 'server']);
}); });

View file

@ -1,14 +1,14 @@
Package.describe({ Package.describe({
name: 'vulcan:i18n', name: 'vulcan:i18n',
summary: 'i18n client polyfill', summary: 'i18n client polyfill',
version: '1.12.17', version: '1.13.0',
git: 'https://github.com/VulcanJS/Vulcan', git: 'https://github.com/VulcanJS/Vulcan',
}); });
Package.onUse(function(api) { Package.onUse(function(api) {
api.versionsFrom('1.6.1'); api.versionsFrom('1.6.1');
api.use(['vulcan:lib@1.12.17']); api.use(['vulcan:lib@1.13.0']);
api.mainModule('lib/server/main.js', 'server'); api.mainModule('lib/server/main.js', 'server');
api.mainModule('lib/client/main.js', 'client'); api.mainModule('lib/client/main.js', 'client');

View file

@ -1,10 +1,11 @@
import { onError } from 'apollo-link-error'; import { onError } from 'apollo-link-error';
const locationsToStr = (locations=[]) => locations.map(({column, line}) => `line ${line}, col ${column}`).join(';');
const errorLink = onError(({ graphQLErrors, networkError }) => { const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) if (graphQLErrors)
graphQLErrors.map(({ message, locations, path }) => { graphQLErrors.map(({ message, locations, path }) => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`); console.log(`[GraphQL error]: Message: ${message}, Location: ${locationsToStr(locations)}, Path: ${path}`);
}); });
if (networkError) { if (networkError) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console

View file

@ -9,7 +9,7 @@ import SimpleSchema from 'simpl-schema';
Vulcan = {}; Vulcan = {};
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
Vulcan.VERSION = '1.12.17'; Vulcan.VERSION = '1.13.0';
// ------------------------------------- Schemas -------------------------------- // // ------------------------------------- Schemas -------------------------------- //

View file

@ -18,6 +18,8 @@ const getFirstWord = input => {
Parse a GraphQL error message Parse a GraphQL error message
TODO: check if still useful?
Sample message: Sample message:
"GraphQL error: Variable "$data" got invalid value {"meetingDate":"2018-08-07T06:05:51.704Z"}. "GraphQL error: Variable "$data" got invalid value {"meetingDate":"2018-08-07T06:05:51.704Z"}.
@ -28,6 +30,11 @@ In field "addresses": Expected "[JSON]!", found null."
*/ */
const parseErrorMessage = message => { const parseErrorMessage = message => {
if (!message) {
return null;
}
// note: optionally add .slice(1) at the end to get rid of the first error, which is not that helpful // note: optionally add .slice(1) at the end to get rid of the first error, which is not that helpful
let fieldErrors = message.split('\n'); let fieldErrors = message.split('\n');
@ -52,6 +59,7 @@ const parseErrorMessage = message => {
}); });
return fieldErrors; return fieldErrors;
}; };
/* /*
Errors can have the following properties stored on their `data` property: Errors can have the following properties stored on their `data` property:
@ -60,37 +68,17 @@ Errors can have the following properties stored on their `data` property:
- properties: additional data. Will be passed to vulcan-i18n as values - properties: additional data. Will be passed to vulcan-i18n as values
- message: if id cannot be used as i81n key, message will be used - message: if id cannot be used as i81n key, message will be used
Scenario 1: normal error thrown with new Error(), put it in array and return it
Scenario 2: multiple GraphQL errors stored on data.errors
Scenario 3: single GraphQL error with data property
Scenario 4: single GraphQL error with no data property
*/ */
export const getErrors = error => { export const getErrors = error => {
// 1. by default, return raw error wrapped in array const graphQLErrors = error.graphQLErrors;
let errors = [error];
// error thrown using new ApolloError
const apolloErrors = get(graphQLErrors, '0.extensions.exception.data.errors');
// if this is one or more GraphQL errors, extract and convert them // regular server error (with schema stitching)
if (error.graphQLErrors && error.graphQLErrors.length > 0) { const regularErrors = get(graphQLErrors, '0.extensions.exception.errors');
// get first graphQL error (see https://github.com/thebigredgeek/apollo-errors/issues/12)
const graphQLError = error.graphQLErrors[0]; return apolloErrors || regularErrors || graphQLErrors;
const data = get(graphQLError, 'extensions.exception.data')
if (data && !isEmpty(data)) {
if (data.errors) {
// 2. there are multiple errors on the data.errors object
errors = data.errors;
} else {
// 3. there is only one error
errors = [data];
}
} else {
// 4. there is no data object, try to parse raw error message
errors = parseErrorMessage(graphQLError.message);
}
}
return errors;
}; };

View file

@ -1,4 +1,4 @@
export * from './apollo_server2'; export * from './apollo_server';
export * from './settings'; export * from './settings';
export { default as initGraphQL } from './initGraphQL'; export { default as initGraphQL } from './initGraphQL';

View file

@ -75,7 +75,8 @@ const initGraphQL = () => {
resolvers: GraphQLSchema.resolvers, resolvers: GraphQLSchema.resolvers,
schemaDirectives: GraphQLSchema.directives, schemaDirectives: GraphQLSchema.directives,
}); });
const mergedSchema = mergeSchemas({ schemas: [executableSchema, ...GraphQLSchema.stitchedSchemas] }); // only call mergeSchemas if we actually have stitchedSchemas
const mergedSchema = GraphQLSchema.stitchedSchemas.length > 0 ? mergeSchemas({ schemas: [executableSchema, ...GraphQLSchema.stitchedSchemas] }) : executableSchema;
GraphQLSchema.finalSchema = typeDefs; GraphQLSchema.finalSchema = typeDefs;
GraphQLSchema.executableSchema = mergedSchema; GraphQLSchema.executableSchema = mergedSchema;

View file

@ -1,3 +1,3 @@
const { onStart } = require('./apollo_server2'); const { onStart } = require('./apollo_server');
// createApolloServer when server startup // createApolloServer when server startup
Meteor.startup(onStart); Meteor.startup(onStart);

View file

@ -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,
});
});
*/

View file

@ -1,4 +1,4 @@
import { ApolloError } from 'apollo-server'; import { UserInputError } from 'apollo-server';
/* /*
@ -11,5 +11,5 @@ An error should have:
*/ */
export const throwError = error => { export const throwError = error => {
const { id, } = error; const { id, } = error;
throw new ApolloError(id, 'VALIDATION_ERROR', error); throw new UserInputError(id, error);
}; };

View file

@ -18,4 +18,6 @@ export * from './intl.js';
export * from './accounts_helpers.js'; export * from './accounts_helpers.js';
export * from './source_version.js'; export * from './source_version.js';
export * from './apollo-server/settings.js';
import './apollo-server/startup'; import './apollo-server/startup';

View file

@ -80,7 +80,7 @@ export const createMutator = async ({
Note: keep newDocument for backwards compatibility Note: keep newDocument for backwards compatibility
*/ */
const properties = { data, currentUser, collection, context, document, newDocument: document }; const properties = { data, originalData: clone(data), currentUser, collection, context, document, newDocument: document, schema };
/* /*
@ -273,7 +273,7 @@ export const updateMutator = async ({
Properties Properties
*/ */
const properties = { data, oldDocument, document, currentUser, collection, context }; const properties = { data, oldDocument, document, currentUser, collection, context, schema };
/* /*
@ -473,7 +473,7 @@ export const deleteMutator = async ({
Properties Properties
*/ */
const properties = { document, currentUser, collection, context }; const properties = { document, currentUser, collection, context, schema };
/* /*
@ -584,7 +584,7 @@ const startDebugMutator = (name, action, properties) => {
}); });
}; };
const endDebugMutator = (name, action, properties) => { const endDebugMutator = (name, action, properties = {}) => {
Object.keys(properties).forEach(p => { Object.keys(properties).forEach(p => {
debug(`// ${p}: `, properties[p]); debug(`// ${p}: `, properties[p]);
}); });

View file

@ -93,9 +93,10 @@ Meteor.startup(() => {
Collections.forEach(collection => { Collections.forEach(collection => {
const typeName = collection.options.typeName; const typeName = collection.options.typeName;
collection.queryOne = async (documentId, { fragmentName, fragmentText, context }) => { collection.queryOne = async (documentIdOrSelector, { fragmentName, fragmentText, context }) => {
const selector = typeof documentIdOrSelector === 'string' ? { documentId: documentIdOrSelector } : documentIdOrSelector;
const query = buildQuery(collection, { fragmentName, fragmentText }); const query = buildQuery(collection, { fragmentName, fragmentText });
const result = await runQuery(query, { input: { selector: { documentId } } }, context); const result = await runQuery(query, { input: { selector } }, context);
return result.data[Utils.camelCaseify(typeName)].result; return result.data[Utils.camelCaseify(typeName)].result;
}; };
}); });

View file

@ -1,7 +1,7 @@
Package.describe({ Package.describe({
name: 'vulcan:lib', name: 'vulcan:lib',
summary: 'Vulcan libraries.', summary: 'Vulcan libraries.',
version: '1.12.17', version: '1.13.0',
git: 'https://github.com/VulcanJS/Vulcan.git', git: 'https://github.com/VulcanJS/Vulcan.git',
}); });

View file

@ -1,4 +1,4 @@
import { createApolloServer } from '../../lib/server/apollo-server/apollo_server2'; import { createApolloServer } from '../../lib/server/apollo-server/apollo_server';
//import initGraphQL from '../../lib/server/apollo-server/initGraphQL'; //import initGraphQL from '../../lib/server/apollo-server/initGraphQL';
//import { GraphQLSchema } from '../../lib/modules/graphql'; //import { GraphQLSchema } from '../../lib/modules/graphql';
import expect from 'expect'; import expect from 'expect';

View file

@ -1,14 +1,14 @@
Package.describe({ Package.describe({
name: 'vulcan:newsletter', name: 'vulcan:newsletter',
summary: 'Vulcan email newsletter package', summary: 'Vulcan email newsletter package',
version: '1.12.17', version: '1.13.0',
git: 'https://github.com/VulcanJS/Vulcan.git', git: 'https://github.com/VulcanJS/Vulcan.git',
}); });
Package.onUse(function(api) { Package.onUse(function(api) {
api.versionsFrom('1.6.1'); api.versionsFrom('1.6.1');
api.use(['vulcan:core@1.12.17', 'vulcan:email@1.12.17']); api.use(['vulcan:core@1.13.0', 'vulcan:email@1.13.0']);
api.mainModule('lib/server/main.js', 'server'); api.mainModule('lib/server/main.js', 'server');
api.mainModule('lib/client/main.js', 'client'); api.mainModule('lib/client/main.js', 'client');

View file

@ -1,14 +1,14 @@
Package.describe({ Package.describe({
name: 'vulcan:payments', name: 'vulcan:payments',
summary: 'Vulcan payments package', summary: 'Vulcan payments package',
version: '1.12.17', version: '1.13.0',
git: 'https://github.com/VulcanJS/Vulcan.git', git: 'https://github.com/VulcanJS/Vulcan.git',
}); });
Package.onUse(function(api) { Package.onUse(function(api) {
api.versionsFrom('1.6.1'); api.versionsFrom('1.6.1');
api.use(['promise', 'vulcan:core@1.12.17', 'fourseven:scss@4.5.4']); api.use(['promise', 'vulcan:core@1.13.0', 'fourseven:scss@4.5.4']);
api.mainModule('lib/server/main.js', 'server'); api.mainModule('lib/server/main.js', 'server');
api.mainModule('lib/client/main.js', 'client'); api.mainModule('lib/client/main.js', 'client');

View file

@ -0,0 +1 @@
Redux package.

View file

@ -1,14 +1,14 @@
Package.describe({ Package.describe({
name: 'vulcan:routing', name: 'vulcan:routing',
summary: 'Vulcan router package', summary: 'Vulcan router package',
version: '1.12.17', version: '1.13.0',
git: 'https://github.com/VulcanJS/Vulcan.git', git: 'https://github.com/VulcanJS/Vulcan.git',
}); });
Package.onUse(function(api) { Package.onUse(function(api) {
api.versionsFrom('1.6.1'); api.versionsFrom('1.6.1');
api.use(['vulcan:lib@1.12.17']); api.use(['vulcan:lib@1.13.0']);
api.mainModule('lib/server/main.js', 'server'); api.mainModule('lib/server/main.js', 'server');
api.mainModule('lib/client/main.js', 'client'); api.mainModule('lib/client/main.js', 'client');

View file

@ -0,0 +1 @@
Styled components package.

View file

@ -1,7 +1,7 @@
Package.describe({ Package.describe({
name: 'vulcan:subscribe', name: 'vulcan:subscribe',
summary: 'Subscribe to posts, users, etc. to be notified of new activity', summary: 'Subscribe to posts, users, etc. to be notified of new activity',
version: '1.12.17', version: '1.13.0',
git: 'https://github.com/VulcanJS/Vulcan.git', git: 'https://github.com/VulcanJS/Vulcan.git',
}); });
@ -9,11 +9,11 @@ Package.onUse(function(api) {
api.versionsFrom('1.6.1'); api.versionsFrom('1.6.1');
api.use([ api.use([
'vulcan:core@1.12.17', 'vulcan:core@1.13.0',
// dependencies on posts, categories are done with nested imports to reduce explicit dependencies // dependencies on posts, categories are done with nested imports to reduce explicit dependencies
]); ]);
api.use(['vulcan:posts@1.12.17', 'vulcan:comments@1.12.17', 'vulcan:categories@1.12.17'], { api.use(['vulcan:posts@1.13.0', 'vulcan:comments@1.13.0', 'vulcan:categories@1.13.0'], {
weak: true, weak: true,
}); });

View file

@ -0,0 +1 @@
Test package.

View file

@ -1,14 +1,14 @@
Package.describe({ Package.describe({
name: 'vulcan:ui-bootstrap', name: 'vulcan:ui-bootstrap',
summary: 'Vulcan Bootstrap UI components.', summary: 'Vulcan Bootstrap UI components.',
version: '1.12.17', version: '1.13.0',
git: 'https://github.com/VulcanJS/Vulcan.git', git: 'https://github.com/VulcanJS/Vulcan.git',
}); });
Package.onUse(function(api) { Package.onUse(function(api) {
api.versionsFrom('1.6.1'); api.versionsFrom('1.6.1');
api.use(['vulcan:lib@1.12.17', 'fourseven:scss@4.10.0']); api.use(['vulcan:lib@1.13.0', 'fourseven:scss@4.10.0']);
api.addFiles(['lib/stylesheets/style.scss', 'lib/stylesheets/datetime.scss'], 'client'); api.addFiles(['lib/stylesheets/style.scss', 'lib/stylesheets/datetime.scss'], 'client');

View file

@ -0,0 +1,6 @@
npm-debug.log
node_modules
.idea/workspace.xml
### eslint-config
.eslintrc

View file

@ -0,0 +1,89 @@
accounts-base@1.4.3
allow-deny@1.1.0
autoupdate@1.5.0
babel-compiler@7.2.4
babel-runtime@1.3.0
base64@1.0.11
binary-heap@1.0.11
blaze-tools@1.0.10
boilerplate-generator@1.6.0
buffer@0.0.0
caching-compiler@1.2.1
caching-html-compiler@1.1.3
callback-hook@1.1.0
check@1.3.1
ddp@1.4.0
ddp-client@2.3.3
ddp-common@1.4.0
ddp-rate-limiter@1.0.7
ddp-server@2.2.0
deps@1.0.12
diff-sequence@1.1.1
dynamic-import@0.5.0
ecmascript@0.12.4
ecmascript-runtime@0.7.0
ecmascript-runtime-client@0.8.0
ecmascript-runtime-server@0.7.1
ejson@1.1.0
email@1.2.3
erikdakoda:vulcan-material-ui@1.12.8_17
es5-shim@4.8.0
fetch@0.1.0
fourseven:scss@4.10.0
geojson-utils@1.0.10
hot-code-push@1.0.4
html-tools@1.0.11
htmljs@1.0.11
http@1.4.1
id-map@1.1.0
inter-process-messaging@0.1.0
localstorage@1.2.0
logging@1.1.20
meteor@1.9.2
meteorhacks:inject-initial@1.0.4
meteorhacks:picker@1.0.3
minifier-css@1.4.1
minifier-js@2.4.0
minimongo@1.4.5
modern-browsers@0.1.3
modules@0.13.0
modules-runtime@0.10.3
mongo@1.6.0
mongo-decimal@0.1.0
mongo-dev-server@1.1.0
mongo-id@1.0.7
npm-mongo@3.1.1
ordered-dict@1.1.0
percolatestudio:synced-cron@1.1.0
promise@0.11.2
random@1.1.0
rate-limit@1.0.9
reactive-dict@1.2.1
reactive-var@1.0.11
reload@1.2.0
retry@1.1.0
routepolicy@1.1.0
server-render@0.3.1
service-configuration@1.0.11
session@1.2.0
shell-server@0.4.0
socket-stream-client@0.2.2
spacebars-compiler@1.1.3
standard-minifier-css@1.5.2
standard-minifier-js@2.4.0
static-html@1.2.2
templating-tools@1.1.2
tracker@1.2.0
underscore@1.0.10
url@1.2.0
vulcan:accounts@1.12.8
vulcan:core@1.12.8
vulcan:debug@1.12.8
vulcan:email@1.12.8
vulcan:forms@1.12.8
vulcan:i18n@1.12.8
vulcan:lib@1.12.8
vulcan:routing@1.12.8
vulcan:users@1.12.8
webapp@1.7.1
webapp-hashing@1.0.9

View file

@ -0,0 +1,18 @@
.accounts-ui .form-control {
color: rgba(0, 0, 0, 0.87);
padding: 8px 0;
font-size: 1rem;
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
border: none;
box-shadow: none;
border-bottom: 1px solid rgba(0, 0, 0, 0.87);
margin-bottom: 1px;
border-radius: 0;
width: 100%;
}
.accounts-ui .form-control:focus {
border-bottom-width: 2px;
margin-bottom: 0;
}

View file

@ -0,0 +1,12 @@
import { addStrings } from 'meteor/vulcan:core';
addStrings('en', {
'search.search': 'Search',
'search.clear': 'Clear search',
'load_more.load_more': 'Load more',
'load_more.loaded_count': 'Loaded {count} of {totalCount}',
'load_more.loaded_all': '{totalCount, plural, =0 {No items} one {One item} other {# items}}',
});

View file

@ -0,0 +1,16 @@
.form-nested-item {
display: flex;
}
.form-nested-item-inner {
flex-grow: 1;
}
.form-nested-item-remove {
padding-top: 8px;
margin-left: 8px;
}
.form-nested-item-remove > button {
margin-right: -4px;
}

View file

@ -0,0 +1,7 @@
import { addStrings } from 'meteor/vulcan:core';
addStrings('fr', {
'search.search': 'Recherche',
'search.clear': 'Effacer la recherche',
'modal.close': 'Fermer',
});

View file

@ -0,0 +1,109 @@
1.12.8_17 / 2019-02-02
======================
* TooltipIntl: Changed display from 'inline-block' to 'inherit' for more flexibility
* Countries: Added getRegionLabel function
1.12.8_16 / 2019-01-21
======================
* Countries: Fixed bug in validateRegion
1.12.8_15 / 2019-01-21
======================
* Countries: Fixed bug in validateRegion
1.12.8_14 / 2019-01-20
======================
* Countries: Added validateRegion function, which given a region value or label, will return the region value ('NY' or 'New York' => 'NY)
* The contents of countries is now exported - this may be refactored out of the core vulcan-material-ui as some point
1.12.8_13 / 2019-01-14
======================
* ModalTrigger: Added boolean dialogOverflow prop for use cases like popups that can go beyond the size of the dialog box
* MuiSuggest: Fixed bug - The disabled state was not displayed correctly
* MuiSuggest: Fixed bug - After selecting a suggestion, clicking on the control did not re-open the suggestions menu
1.12.8_12 / 2019-01-12
======================
* Upgraded to Meteor 1.8.0.2
1.12.8_11 / 2018-12-21
======================
* SearchInput: Added install autosize-input to readme
* Datatable: Fixed sorting delay
* Datatable: Added tableHeadCell class
* Datatable: Added cellClass column property, which can be a string or a function: column.cellClass({ column, document, currentUser })
1.12.8_10 / 2018-12-09
======================
* TooltipIntl: Added icon class
* FormGroupWithLine: Moved caret from the right side to next to the title
* Changed load_more.loaded_all string
1.12.8_9 / 2018-11-26
=====================
* Fixed bug that displayed invalid total count at the bottom of data tables
1.12.8_8 / 2018-11-23
=====================
* Improved the functionality of the LoadMore component
* The showNoMore property has been deprecated
* A showCount property has been added (true by default) that shows a count of loaded and total items
* The load more icon or button is displayed even when infiniteScroll is enabled
1.12.8_7 / 2018-11-10
=====================
* Fixed bug in Datatable.jsx
* Updated ReadMe
1.12.8_6 / 2018-11-06
=====================
* Fixed bug in Datatable.jsx
* Reduced spacing of form components
1.12.8_5 / 2018-10-31
=====================
* Fixed bugs in Datatable pagination
* Set Datatable paginate prop to false by default
1.12.8_4 / 2018-10-31
=====================
* Removed 'fr_FR.js' from package.js because any french strings loaded activates the french language
* Fixed delete button and its tooltips positioning in FormSubmit
* Added pagination to Datatable
1.12.8_2 / 2018-10-29
=====================
* Fixed localization in "clear search" tooltip
* Added name and aria-haspopup properties to the input component to improve compliance and facilitate UAT
* Replaced Date, Time and DateTime form controls with native controls as recommended by MUI.
The deprecated react-datetime version of the controls are still there as DateRdt, TimeRdt and DateTimeRdt, but they are not registered.
* Updated readme
1.12.8_1 / 2018-10-22
=====================
* Made form components compatible with new Form.formComponents property
1.12.8 / 2018-10-19
===================
* Made improvements to the search box, including keyboard shortcuts (s: focus search; c: clear search)
* Added support in TooltipIntl for tooltips in popovers
* Added action prop to ModalTrigger that enables a parent component to call openModal and closeModal
* Started using MUI tables in Card component
* Fixed bugs in MuiSuggest component

View file

@ -0,0 +1,2 @@
export * from '../modules/index';
import './wrapWithMuiTheme';

View file

@ -0,0 +1,29 @@
import React from 'react';
import { addCallback, registerComponent, Components } from 'meteor/vulcan:core';
import MuiThemeProvider from '@material-ui/core/styles/MuiThemeProvider';
import { getCurrentTheme } from '../modules/themes';
import JssCleanup from '../components/theme/JssCleanup';
class ThemeProvider extends React.Component {
render() {
const theme = getCurrentTheme();
return (
<MuiThemeProvider theme={theme}>
<JssCleanup>{this.props.children}</JssCleanup>
</MuiThemeProvider>
);
}
}
registerComponent('ThemeProvider', ThemeProvider);
function wrapWithMuiTheme(app) {
return (
<Components.ThemeProvider>
{app}
</Components.ThemeProvider>
);
}
addCallback('router.client.wrapper', wrapWithMuiTheme);

View file

@ -0,0 +1,46 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Button from '@material-ui/core/Button';
import { replaceComponent, Utils } from 'meteor/vulcan:core';
import classNames from 'classnames';
export class AccountsButton extends Component {
render () {
const {
label,
type,
disabled = false,
className,
onClick
} = this.props;
return (
<Button
variant={type === 'link' ? 'text' : 'contained'}
size={type === 'link' ? 'small' : undefined}
color="primary"
className={classNames(`button-${Utils.slugify(label)}`, className)}
type={type}
disabled={disabled}
onClick={onClick}
disableRipple={true}
>
{label}
</Button>
);
}
}
AccountsButton.propTypes = {
label: PropTypes.string.isRequired,
type: PropTypes.oneOf(['link', 'submit', 'button']),
disabled: PropTypes.bool,
className: PropTypes.string,
onClick: PropTypes.func.isRequired,
};
replaceComponent('AccountsButton', AccountsButton);

View file

@ -0,0 +1,48 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Components, replaceComponent } from 'meteor/vulcan:core';
import CardActions from '@material-ui/core/CardActions';
import withStyles from '@material-ui/core/styles/withStyles';
import classNames from 'classnames';
const styles = theme => ({
root: {
flexDirection: 'row-reverse',
padding: theme.spacing.unit * 2,
height: 'auto',
},
});
export class AccountsButtons extends Component {
render () {
const {
classes,
buttons = {},
className = 'buttons',
} = this.props;
return (
<CardActions className={classNames(classes.root, className)}>
{Object.keys(buttons).map((id, i) =>
<Components.AccountsButton {...buttons[id]} key={i}/>
)}
</CardActions>
);
}
}
AccountsButtons.propTypes = {
classes: PropTypes.object.isRequired,
buttons: PropTypes.object,
className: PropTypes.string,
};
AccountsButtons.displayName = 'AccountsButtons';
replaceComponent('AccountsButtons', AccountsButtons, [withStyles, styles]);

View file

@ -0,0 +1,114 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { replaceComponent } from 'meteor/vulcan:core';
import TextField from '@material-ui/core/TextField';
const autocompleteValues = {
'username': 'username',
'usernameOrEmail': 'email',
'email': 'email',
'password': 'current-password'
};
export class AccountsField extends PureComponent {
constructor (props) {
super(props);
this.state = {
mount: true
};
}
triggerUpdate () {
// Trigger an onChange on initial load, to support browser pre-filled values.
const { onChange } = this.props;
if (this.input && onChange) {
onChange({ target: { value: this.input.value } });
}
}
componentDidMount () {
this.triggerUpdate();
}
componentDidUpdate (prevProps) {
// Re-mount component so that we don't expose browser pre-filled passwords if the component was
// a password before and now something else.
if (prevProps.id !== this.props.id) {
this.setState({ mount: false });
} else if (!this.state.mount) {
this.setState({ mount: true });
this.triggerUpdate();
}
}
render () {
const {
id,
hint,
label,
type = 'text',
onChange,
required = false,
className = 'field',
defaultValue = '',
autoFocus,
messages,
} = this.props;
let { message } = this.props;
const { mount = true } = this.state;
if (type === 'notice') {
return <div className={className}>{label}</div>;
}
const autoComplete = autocompleteValues[id];
if (messages && messages.find && typeof id === 'string') {
const foundMessage = messages.find(element => {
if (typeof element.field !== 'string') return false;
return id.toLowerCase().indexOf(element.field.toLowerCase()) > -1;
});
if (foundMessage) {
message = foundMessage;
}
}
return (
mount &&
<div className={className} style={{ marginBottom: '10px' }}>
<TextField
id={id}
type={type}
inputRef={ref => { this.input = ref; }}
onChange={onChange}
placeholder={hint}
defaultValue={defaultValue}
autoComplete={autoComplete }
label={label}
autoFocus={autoFocus}
required={required}
error={!!message}
helperText={message && message.message}
fullWidth
/>
</div>
);
}
}
AccountsField.propTypes = {
onChange: PropTypes.func,
};
replaceComponent('AccountsField', AccountsField);

View file

@ -0,0 +1,27 @@
import React, { Component } from 'react';
import { Components, replaceComponent } from 'meteor/vulcan:core';
import CardContent from '@material-ui/core/CardContent';
export class AccountsFields extends Component {
render () {
const {
fields = {},
className = 'fields',
messages,
} = this.props;
return (
<CardContent className={className}>
{
Object.keys(fields).map((id, i) =>
<Components.AccountsField {...fields[id]} messages={messages} autoFocus={i === 0} key={i}/>
)
}
</CardContent>
);
}
}
replaceComponent('AccountsFields', AccountsFields);

View file

@ -0,0 +1,68 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Components, registerComponent } from 'meteor/vulcan:core';
import withStyles from '@material-ui/core/styles/withStyles';
const styles = theme => ({
messages: theme.utils.errorMessage,
});
export class AccountsForm extends Component {
componentDidMount () {
let form = this.form;
if (form) {
form.addEventListener('submit', (e) => {
e.preventDefault();
});
}
}
render () {
const {
oauthServices,
fields,
buttons,
messages,
ready = true,
className,
classes,
} = this.props;
return (
<form ref={(ref) => this.form = ref}
className={classNames(className, 'accounts-ui', { 'ready': ready, })}
noValidate
>
<Components.AccountsFields fields={fields} messages={messages}/>
<Components.AccountsButtons buttons={{...buttons}}/>
<Components.AccountsPasswordOrService oauthServices={oauthServices}/>
<Components.AccountsSocialButtons oauthServices={oauthServices}/>
<Components.AccountsFormMessages messages={messages} className={classes.messages}/>
</form>
);
}
}
AccountsForm.propTypes = {
oauthServices: PropTypes.object,
fields: PropTypes.object.isRequired,
buttons: PropTypes.object.isRequired,
error: PropTypes.string,
ready: PropTypes.bool,
classes: PropTypes.object.isRequired,
};
AccountsForm.displayName = 'AccountsForm';
registerComponent('AccountsForm', AccountsForm, [withStyles, styles]);

View file

@ -0,0 +1,57 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { replaceComponent } from 'meteor/vulcan:core';
import { intlShape } from 'meteor/vulcan:i18n';
import Typography from '@material-ui/core/Typography';
import CardActions from '@material-ui/core/CardActions';
import withStyles from '@material-ui/core/styles/withStyles';
import classNames from 'classnames';
const styles = theme => ({
root: {
flexDirection: 'row-reverse',
paddingRight: theme.spacing.unit * 2,
paddingLeft: theme.spacing.unit * 2,
height: 'auto',
},
typography: {
marginRight: theme.spacing.unit,
}
});
export function hasPasswordService() {
// First look for OAuth services.
return !!Package['accounts-password'];
}
export class AccountsPasswordOrService extends PureComponent {
render () {
let { className = 'password-or-service', style = {}, classes } = this.props;
const services = Object.keys(this.props.oauthServices).map(service => {
return this.props.oauthServices[service].label;
});
let labels = services;
if (services.length > 2) {
labels = [];
}
if (hasPasswordService() && services.length > 0) {
return (<CardActions className={classNames(className, classes.root)}>
<Typography variant="caption" className={classes.typography} align="right">
{ `${this.context.intl.formatMessage({id: 'accounts.or_use'})} ${ labels.join(' / ') }` }
</Typography></CardActions>
);
}
return null;
}
}
AccountsPasswordOrService.propTypes = {
oauthServices: PropTypes.object
};
AccountsPasswordOrService.contextTypes = {
intl: intlShape
};
replaceComponent('AccountsPasswordOrService', AccountsPasswordOrService, [withStyles, styles]);

View file

@ -0,0 +1,28 @@
import React from 'react';
import { Components, replaceComponent } from 'meteor/vulcan:core';
import CardActions from '@material-ui/core/CardActions';
import withStyles from '@material-ui/core/styles/withStyles';
import classNames from 'classnames';
const styles = theme => ({
root: {
justifyContent: 'flex-end',
padding: theme.spacing.unit * 2,
height: 'auto',
},
});
export class AccountsSocialButtons extends React.Component {
render() {
let { oauthServices = {}, className = 'social-buttons', classes } = this.props;
return(
<CardActions className={classNames(classes.root, className)}>
{Object.keys(oauthServices).map((id, i) => {
return <Components.AccountsButton {...oauthServices[id]} key={i} />;
})}
</CardActions>
);
}
}
replaceComponent('AccountsSocialButtons', AccountsSocialButtons, [withStyles, styles]);

View file

@ -0,0 +1,133 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, intlShape } from 'react-intl';
import { Components, registerComponent } from 'meteor/vulcan:core';
import withStyles from '@material-ui/core/styles/withStyles';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import ArrowDownIcon from 'mdi-material-ui/ArrowDown';
import ScrollTrigger from './ScrollTrigger';
import classNames from 'classnames';
const styles = theme => ({
root: {
textAlign: 'center',
flexBasis: '100%',
},
textButton: {
marginTop: theme.spacing.unit * 2,
},
iconButton: {},
caption: {
marginTop: theme.spacing.unit * 3,
paddingTop: theme.spacing.unit,
paddingBottom: theme.spacing.unit,
},
});
const LoadMore = ({
classes,
count,
totalCount,
loadMore,
networkStatus,
showCount,
useTextButton,
className,
infiniteScroll,
}, { intl }) => {
const isLoadingMore = networkStatus === 2;
const loadMoreText = intl.formatMessage({ id: 'load_more.load_more' });
const title = `${loadMoreText} (${count}/${totalCount})`;
const hasMore = totalCount > count;
const countValues = { count, totalCount };
const loadMoreButton = useTextButton
?
<Button className={classes.textButton} onClick={() => loadMore()}>
{title}
</Button>
:
<IconButton className={classes.iconButton} onClick={() => loadMore()}>
<ArrowDownIcon/>
</IconButton>;
return (
<div className={classNames('load-more', classes.root, className)}>
{
showCount &&
<Typography variant="caption" className={classes.caption}>
<FormattedMessage id={`load_more.${hasMore ? 'loaded_count' : 'loaded_all'}`} values={countValues}/>
</Typography>
}
{
isLoadingMore
?
<Components.Loading/>
:
hasMore
?
infiniteScroll
?
<ScrollTrigger onEnter={() => loadMore()}>
{loadMoreButton}
</ScrollTrigger>
:
loadMoreButton
:
null
}
</div>
);
};
LoadMore.propTypes = {
classes: PropTypes.object.isRequired,
count: PropTypes.number,
totalCount: PropTypes.number,
loadMore: PropTypes.func,
networkStatus: PropTypes.number,
showCount: PropTypes.bool,
useTextButton: PropTypes.bool,
className: PropTypes.string,
infiniteScroll: PropTypes.bool,
};
LoadMore.defaultProps = {
showCount: true,
};
LoadMore.contextTypes = {
intl: intlShape.isRequired,
};
LoadMore.displayName = 'LoadMore';
registerComponent('LoadMore', LoadMore, [withStyles, styles]);

View file

@ -0,0 +1,125 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import _throttle from 'lodash/throttle';
class ScrollTrigger extends Component {
constructor (props) {
super(props);
this.onScroll = _throttle(this.onScroll.bind(this), 100, {
leading: true,
trailing: true,
});
this.onResize = _throttle(this.onResize.bind(this), 100, {
leading: true,
trailing: true,
});
this.inViewport = false;
this.passive = this.supportsPassive ? { passive: true } : false;
}
supportsPassive () {
let supportsPassive = false;
try {
const opts = Object.defineProperty({}, 'passive', {
get: function() {
supportsPassive = true;
}
});
window.addEventListener('testPassive', null, opts);
window.removeEventListener('testPassive', null, opts);
//eslint-disable-next-line no-empty
} catch (e) {}
return supportsPassive;
}
componentDidMount () {
this.scroller = document.getElementById('main');
this.scroller.addEventListener('resize', this.onResize, this.passive);
this.scroller.addEventListener('scroll', this.onScroll, this.passive);
this.inViewport = false;
if (this.props.triggerOnLoad) {
this.checkStatus();
}
}
componentWillUnmount () {
if (!this.scroller) return;
this.scroller.removeEventListener('resize', this.onResize);
this.scroller.removeEventListener('scroll', this.onScroll);
this.scroller = null;
}
onResize () {
this.checkStatus();
}
onScroll () {
this.checkStatus();
}
checkStatus () {
if (!this.scroller) return;
const {
onEnter,
} = this.props;
//eslint-disable-next-line
const element = ReactDOM.findDOMNode(this.element);
const elementRect = element.getBoundingClientRect();
const viewportEnd = this.scroller.clientHeight + this.props.preload;
const inViewport = elementRect.top < viewportEnd;
if (inViewport) {
if (!this.inViewport) {
this.inViewport = true;
onEnter(this);
}
} else {
if (this.inViewport) {
this.inViewport = false;
}
}
}
render () {
const {
children,
} = this.props;
return (
<div ref={(element) => {this.element = element;}}>
{children}
</div>
);
}
}
ScrollTrigger.propTypes = {
scrollerId: PropTypes.string,
triggerOnLoad: PropTypes.bool,
preload: PropTypes.number,
onEnter: PropTypes.func,
};
ScrollTrigger.defaultProps = {
scrollerId: 'main',
preload: 1000,
triggerOnLoad: true,
onEnter: () => {},
};
export default ScrollTrigger;

View file

@ -0,0 +1,228 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { Components, registerComponent } from 'meteor/vulcan:core';
import withStyles from '@material-ui/core/styles/withStyles';
import SearchIcon from 'mdi-material-ui/Magnify';
import ClearIcon from 'mdi-material-ui/CloseCircle';
import TextField from '@material-ui/core/TextField';
import NoSsr from '@material-ui/core/NoSsr';
import classNames from 'classnames';
import _debounce from 'lodash/debounce';
import KeyboardEventHandler from 'react-keyboard-event-handler';
const styles = theme => ({
'@global': {
'input[type=text]::-ms-clear, input[type=text]::-ms-reveal':
{
display: 'none',
width: 0,
height: 0,
},
'input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button':
{ display: 'none' },
'input[type="search"]::-webkit-search-results-button, input[type="search"]::-webkit-search-results-decoration':
{ display: 'none' },
},
root: {
marginTop: 0
},
clear: {
transition: theme.transitions.create('opacity,transform', {
duration: theme.transitions.duration.short,
}),
opacity: 0.65,
width: 36,
height: 36,
margin: -6,
marginLeft: 0,
'& svg': {
width: 16,
height: 16,
},
flexDirection: 'column',
},
clearDense: {
width: 32,
height: 32,
margin: -4,
marginLeft: 0,
},
clearDisabled: {
opacity: 0,
pointerEvents: 'none',
},
icon: {
color: theme.palette.common.lightBlack,
marginLeft: theme.spacing.unit,
marginRight: theme.spacing.unit,
},
input: {
lineHeight: 1,
paddingTop: 2,
paddingBottom: 2,
marginBottom: 1,
/*transition: theme.transitions.create('width', {
duration: theme.transitions.duration.shortest,
}),*/
minWidth: 130,
},
});
class SearchInput extends PureComponent {
constructor (props) {
super(props);
this.state = {
value: props.defaultValue || '',
};
this.input = null;
this.updateQuery = _debounce(this.updateQuery, 500);
}
componentDidMount () {
if (!document) return;
const element = document.querySelector(`.search-input-${this.props.name} input[type=search]`);
element._addEventListener = element.addEventListener;
element.addEventListener = function(type, listener, useCapture) {
if(useCapture === undefined)
useCapture = false;
this._addEventListener(type, listener, useCapture);
};
element.addEventListener = element._addEventListener;
}
componentWillUnmount () {
}
handleShortcutKeys = (key, event) => {
switch (key) {
case 's':
this.focusInput();
event.preventDefault();
break;
case 'c':
case 'esc':
this.clearSearch(event, true);
event.preventDefault();
break;
}
};
handleFocus = () => {
this.input.select();
};
focusInput = (event) => {
this.input.focus();
};
clearSearch = (event, dontFocus) => {
this.setState({ value: '' });
this.updateQuery('');
if (!dontFocus) {
this.focusInput();
}
};
updateSearch = (event) => {
const value = event.target.value;
this.setState({ value: value });
this.updateQuery(value);
};
updateQuery = (value) => {
this.props.updateQuery(value);
};
render () {
const {
classes,
className,
dense,
noShortcuts,
name,
} = this.props;
const searchIcon = <SearchIcon className={classes.icon} onClick={this.focusInput}/>;
const clearButton = <Components.TooltipIntl
titleId="search.clear"
icon={<ClearIcon/>}
onClick={this.clearSearch}
classes={{
root: classNames(!this.state.value && classes.clearDisabled),
button: classNames('clear-button', classes.clear, dense && classes.clearDense),
}}
disabled={!this.state.value}
/>;
return (
<React.Fragment>
<TextField
label="Search"
type="search"
id={`search-input-${name}`}
name={name}
title="Search"
value={this.state.value}
inputRef={input => this.input = input}
fullWidth
className={classNames('search-input', `search-input-${name}`, classes.root, dense && classes.inputTypeSearch, className, classes.textField)}
margin="normal"
variant="outlined"
onChange={this.updateSearch}
onFocus={this.handleFocus}
InputProps={{
startAdornment: searchIcon,
endAdornment: clearButton
}}
/>
<NoSsr>
{
// KeyboardEventHandler is not valid on the server, where its name is undefined
typeof window !== 'undefined' && KeyboardEventHandler.name && !noShortcuts &&
<KeyboardEventHandler handleKeys={['s', 'c', 'esc']} onKeyEvent={this.handleShortcutKeys}/>
}
</NoSsr>
</React.Fragment>
);
}
}
SearchInput.propTypes = {
classes: PropTypes.object.isRequired,
updateQuery: PropTypes.func.isRequired,
className: PropTypes.string,
dense: PropTypes.bool,
noShortcuts: PropTypes.bool,
name: PropTypes.string.isRequired,
};
SearchInput.defaultProps = {
name: 'search',
};
SearchInput.displayName = 'SearchInput';
registerComponent('SearchInput', SearchInput, [withStyles, styles]);

View file

@ -0,0 +1,108 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Components, registerComponent, Utils } from 'meteor/vulcan:core';
import { intlShape } from 'meteor/vulcan:i18n';
import withStyles from '@material-ui/core/styles/withStyles';
import withTheme from '@material-ui/core/styles/withTheme';
import Tooltip from '@material-ui/core/Tooltip';
import IconButton from '@material-ui/core/IconButton';
import Button from '@material-ui/core/Button';
import classNames from 'classnames';
const styles = theme => ({
root: {},
tooltip: {
margin: '4px !important',
},
buttonWrap: {
display: 'inline-block',
},
button: {},
});
const TooltipIconButton = (props, { intl }) => {
const {
title,
titleId,
placement,
icon,
className,
classes,
theme,
buttonRef,
variant,
...properties
} = props;
const titleText = props.title || intl.formatMessage({ id: titleId });
const slug = Utils.slugify(titleId);
return (
<Tooltip classes={{ tooltip: classNames('tooltip-icon-button', classes.tooltip, className) }}
id={`tooltip-${slug}`}
title={titleText}
placement={placement}
enterDelay={theme.utils.tooltipEnterDelay}
>
<div className={classes.buttonWrap}>
{
variant === 'fab'
?
<Button className={classNames(classes.button, slug)}
variant="fab"
aria-label={title}
ref={buttonRef}
{...properties}
>
{icon}
</Button>
:
<IconButton className={classNames(classes.button, slug)}
aria-label={title}
ref={buttonRef}
{...properties}
>
{icon}
</IconButton>
}
</div>
</Tooltip>
);
};
TooltipIconButton.propTypes = {
title: PropTypes.node,
titleId: PropTypes.string,
placement: PropTypes.string,
icon: PropTypes.node.isRequired,
className: PropTypes.string,
classes: PropTypes.object,
buttonRef: PropTypes.func,
variant: PropTypes.string,
theme: PropTypes.object,
};
TooltipIconButton.defaultProps = {
placement: 'bottom',
};
TooltipIconButton.contextTypes = {
intl: intlShape.isRequired,
};
TooltipIconButton.displayName = 'TooltipIconButton';
registerComponent('TooltipIconButton', TooltipIconButton, [withStyles, styles], [withTheme]);

View file

@ -0,0 +1,168 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Components, registerComponent, Utils } from 'meteor/vulcan:core';
import { intlShape } from 'meteor/vulcan:i18n';
import withStyles from '@material-ui/core/styles/withStyles';
import withTheme from '@material-ui/core/styles/withTheme';
import Tooltip from '@material-ui/core/Tooltip';
import IconButton from '@material-ui/core/IconButton';
import Button from '@material-ui/core/Button';
import classNames from 'classnames';
const styles = theme => ({
root: {
display: 'inherit',
},
tooltip: {
margin: '4px !important',
},
buttonWrap: {
display: 'inherit',
},
button: {},
icon: {},
popoverPopper: {
zIndex: 1700,
},
popoverTooltip: {
zIndex: 1701,
},
});
const TooltipIntl = (props, { intl }) => {
const {
title,
titleId,
titleValues,
placement,
icon,
className,
classes,
theme,
enterDelay,
leaveDelay,
buttonRef,
variant,
parent,
children,
...properties
} = props;
const iconWithClass = icon && React.cloneElement(icon, { className: classes.icon });
const popperClass = parent === 'popover' && classes.popoverPopper;
const tooltipClass = parent === 'popover' && classes.popoverTooltip;
const tooltipEnterDelay = typeof enterDelay === 'number' ? enterDelay : theme.utils.tooltipEnterDelay;
const tooltipLeaveDelay = typeof leaveDelay === 'number' ? leaveDelay : theme.utils.tooltipLeaveDelay;
const titleText = props.title || intl.formatMessage({ id: titleId }, titleValues);
const slug = Utils.slugify(titleId);
return (
<span className={classNames('tooltip-intl', classes.root, className)}>
<Tooltip id={`tooltip-${slug}`}
title={titleText}
placement={placement}
enterDelay={tooltipEnterDelay}
leaveDelay={tooltipLeaveDelay}
classes={{
tooltip: classNames(classes.tooltip, tooltipClass),
popper: popperClass,
}}
>
<span className={classes.buttonWrap}>
{
variant === 'fab' && !!icon
?
<Button className={classNames(classes.button, slug)}
variant="fab"
aria-label={title}
ref={buttonRef}
{...properties}
>
{iconWithClass}
</Button>
:
!!icon
?
<IconButton className={classNames(classes.button, slug)}
aria-label={title}
ref={buttonRef}
{...properties}
>
{iconWithClass}
</IconButton>
:
variant === 'button'
?
<Button className={classNames(classes.button, slug)}
aria-label={title}
ref={buttonRef}
{...properties}
>
{children}
</Button>
:
children
}
</span>
</Tooltip>
</span>
);
};
TooltipIntl.propTypes = {
title: PropTypes.node,
titleId: PropTypes.string,
titleValues: PropTypes.object,
placement: PropTypes.string,
icon: PropTypes.node,
className: PropTypes.string,
classes: PropTypes.object,
buttonRef: PropTypes.func,
variant: PropTypes.string,
theme: PropTypes.object,
enterDelay: PropTypes.number,
leaveDelay: PropTypes.number,
parent: PropTypes.oneOf(['default', 'popover']),
children: PropTypes.node,
};
TooltipIntl.defaultProps = {
placement: 'bottom',
parent: 'default',
};
TooltipIntl.contextTypes = {
intl: intlShape.isRequired,
};
TooltipIntl.displayName = 'TooltipIntl';
registerComponent('TooltipIntl', TooltipIntl, [withStyles, styles], [withTheme]);

View file

@ -0,0 +1,212 @@
import React from 'react';
import PropTypes from 'prop-types';
import { intlShape } from 'meteor/vulcan:i18n';
import { replaceComponent, Components } from 'meteor/vulcan:core';
import moment from 'moment';
import withStyles from '@material-ui/core/styles/withStyles';
import IconButton from '@material-ui/core/IconButton';
import Checkbox from '@material-ui/core/Checkbox';
import EditIcon from 'mdi-material-ui/Pencil';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableRow from '@material-ui/core/TableRow';
import TableCell from '@material-ui/core/TableCell';
import classNames from 'classnames';
const getLabel = (field, fieldName, collection, intl) => {
const schema = collection.simpleSchema()._schema;
const fieldSchema = schema[fieldName];
if (fieldSchema) {
return intl.formatMessage(
{ id: `${collection._name}.${fieldName}`, defaultMessage: fieldSchema.label });
} else {
return fieldName;
}
};
const getTypeName = (field, fieldName, collection) => {
const schema = collection.simpleSchema()._schema;
const fieldSchema = schema[fieldName];
if (fieldSchema) {
const type = fieldSchema.type.singleType;
const typeName = typeof type === 'function' ? type.name : type;
return typeName;
} else {
return typeof field;
}
};
const parseImageUrl = value => {
const isImage = ['.png', '.jpg', '.gif'].indexOf(value.substr(-4)) !== -1 ||
['.webp', '.jpeg'].indexOf(value.substr(-5)) !== -1;
return isImage ?
<img style={{ width: '100%', maxWidth: 200 }} src={value} alt={value}/> :
<LimitedString string={value}/>;
};
const LimitedString = ({ string }) =>
<div>
{string.indexOf(' ') === -1 && string.length > 30 ?
<span title={string}>{string.substr(0, 30)}</span> :
<span>{string}</span>
}
</div>;
export const getFieldValue = (value, typeName, classes={}) => {
if (typeof value === 'undefined' || value === null) {
return '';
}
if (Array.isArray(value)) {
typeName = 'Array';
}
if (typeof typeName === 'undefined') {
typeName = typeof value;
}
switch (typeName) {
case 'Boolean':
case 'boolean':
return <Checkbox checked={value} disabled style={{ width: '32px', height: '32px' }}/>;
case 'Number':
case 'number':
case 'SimpleSchema.Integer':
return <code>{value.toString()}</code>;
case 'Array':
return <ol>{value.map(
(item, index) => <li key={index}>{getFieldValue(item, typeof item, classes)}</li>)}</ol>;
case 'Object':
case 'object':
return (
<Table className="table">
<TableBody>
{_.map(value, (value, key) =>
<TableRow className={classNames(classes.table, 'table')} key={key}>
<TableCell className={classNames(classes.tableHeadCell, 'datacard-label')} variant="head">{key}</TableCell>
<TableCell className={classNames(classes.tableCell, 'datacard-value')} >{getFieldValue(value, typeof value, classes)}</TableCell>
</TableRow>
)}
</TableBody>
</Table>
);
case 'Date':
return moment(new Date(value)).format('dddd, MMMM Do YYYY, h:mm:ss');
default:
return parseImageUrl(value);
}
};
const CardItem = ({ label, value, typeName, classes }) =>
<TableRow className={classes.tableRow}>
<TableCell className={classNames(classes.tableHeadCell, 'datacard-label')} variant="head">
{label}
</TableCell>
<TableCell className={classNames(classes.tableCell, 'datacard-value')}>
{getFieldValue(value, typeName, classes)}
</TableCell>
</TableRow>;
const CardEdit = (props, context) => {
const classes = props.classes;
const editTitle = context.intl.formatMessage({ id: 'cards.edit' });
return (
<TableRow className={classes.tableRow}>
<TableCell className={classes.tableCell} colSpan="2">
<Components.ModalTrigger label={editTitle}
component={<IconButton aria-label={editTitle}>
<EditIcon/>
</IconButton>}
>
<CardEditForm {...props} />
</Components.ModalTrigger>
</TableCell>
</TableRow>
);
};
CardEdit.contextTypes = { intl: intlShape };
const CardEditForm = ({ collection, document, closeModal }) =>
<Components.SmartForm
collection={collection}
documentId={document._id}
showRemove={true}
successCallback={document => {
closeModal();
}}
/>;
const styles = theme => ({
root: {},
table: {
maxWidth: '100%'
},
tableBody: {},
tableRow: {},
tableCell: {},
tableHeadCell: {},
});
const Card = ({ className, collection, document, currentUser, fields, classes }, { intl }) => {
const fieldNames = fields ? fields : _.without(_.keys(document), '__typename');
const canUpdate = currentUser && collection.options.mutations.update.check(currentUser, document);
return (
<div className={classNames(classes.root, 'datacard', `datacard-${collection._name}`, className)}>
<Table className={classNames(classes.table, 'table')} style={{ maxWidth: '100%' }}>
<TableBody>
{canUpdate ? <CardEdit collection={collection} document={document} classes={classes}/> : null}
{fieldNames.map((fieldName, index) =>
<CardItem key={index}
value={document[fieldName]}
typeName={getTypeName(document[fieldName], fieldName, collection)}
label={getLabel(document[fieldName], fieldName, collection, intl)}
classes={classes}
/>
)}
</TableBody>
</Table>
</div>
);
};
Card.displayName = 'Card';
Card.propTypes = {
className: PropTypes.string,
collection: PropTypes.object,
document: PropTypes.object,
currentUser: PropTypes.object,
fields: PropTypes.array,
classes: PropTypes.object.isRequired,
};
Card.contextTypes = {
intl: intlShape
};
replaceComponent('Card', Card, [withStyles, styles]);

View file

@ -0,0 +1,649 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import {
Components,
registerComponent,
replaceComponent,
withCurrentUser,
withMulti,
Utils
} from 'meteor/vulcan:core';
import { intlShape } from 'meteor/vulcan:i18n';
import withStyles from '@material-ui/core/styles/withStyles';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import TableCell from '@material-ui/core/TableCell';
import TableFooter from '@material-ui/core/TableFooter';
import Tooltip from '@material-ui/core/Tooltip';
import TableSortLabel from '@material-ui/core/TableSortLabel';
import TablePagination from '@material-ui/core/TablePagination';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import { getFieldValue } from './Card';
import _assign from 'lodash/assign';
import classNames from 'classnames';
/*
Datatable Component
*/
const baseStyles = theme => ({
root: {
position: 'relative',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
},
addButton: {
top: '9.5rem',
right: '2rem',
position: 'fixed',
bottom: 'auto',
},
table: {
marginTop:0
},
denseTable: {},
denserTable: {},
flatTable: {},
tableHead: {},
tableBody: {},
tableFooter: {},
tableRow: {},
tableHeadCell: {},
tableCell: {},
clickRow: {},
editCell: {},
editButton: {}
});
const delay = (function () {
var timer = 0;
return function (callback, ms) {
clearTimeout(timer);
timer = setTimeout(callback, ms);
};
})();
class Datatable extends PureComponent {
constructor (props) {
super(props);
this.updateQuery = this.updateQuery.bind(this);
this.state = {
value: '',
query: '',
currentSort: {},
};
}
toggleSort = column => {
let currentSort;
if (!this.state.currentSort[column]) {
currentSort = { [column]: 1 };
} else if (this.state.currentSort[column] === 1) {
currentSort = { [column]: -1 };
} else {
currentSort = {};
}
this.setState({ currentSort });
};
updateQuery (value) {
this.setState({
value: value
});
delay(() => {
this.setState({
query: value
});
}, 700);
}
render () {
if (this.props.data) {
return <Components.DatatableContents
columns={this.props.data.length ? Object.keys(this.props.data[0]) : undefined}
{...this.props}
results={this.props.data}
count={this.props.data.length}
totalCount={this.props.data.length}
showEdit={false}
showNew={false}
/>;
} else {
const {
className,
collection,
options,
showSearch,
showNew,
currentUser,
classes,
} = this.props;
const listOptions = {
collection: collection,
...options,
};
const DatatableWithMulti = withMulti(listOptions)(Components.DatatableContents);
// add _id to orderBy when we want to sort a column, to avoid breaking the graphql() hoc;
// see https://github.com/VulcanJS/Vulcan/issues/2090#issuecomment-433860782
// this.state.currentSort !== {} is always false, even when console.log(this.state.currentSort) displays
// {}. So we test on the length of keys for this object.
const orderBy = Object.keys(this.state.currentSort).length == 0 ? {} :
{ ...this.state.currentSort, _id: -1 };
return (
<div className={classNames('datatable', `datatable-${collection._name}`, classes.root,
className)}>
{/* DatatableAbove Component part*/}
{
showSearch &&
<Components.SearchInput value={this.state.query}
updateQuery={this.updateQuery}
className={classes.search}
labelId={'datatable.search'}
/>
}
{
showNew &&
<Components.NewButton collection={collection}
variant="fab"
color="primary"
className={classes.addButton}
/>
}
<DatatableWithMulti {...this.props}
collection={collection}
terms={{ query: this.state.query, orderBy: orderBy }}
currentUser={this.props.currentUser}
toggleSort={this.toggleSort}
currentSort={this.state.currentSort}
/>
</div>
);
}
}
}
Datatable.propTypes = {
title: PropTypes.string,
className: PropTypes.string,
collection: PropTypes.object,
options: PropTypes.object,
columns: PropTypes.array,
showEdit: PropTypes.bool,
editComponent: PropTypes.func,
showNew: PropTypes.bool,
showSearch: PropTypes.bool,
emptyState: PropTypes.node,
currentUser: PropTypes.object,
classes: PropTypes.object,
data: PropTypes.array,
footerData: PropTypes.array,
dense: PropTypes.string,
queryDataRef: PropTypes.func,
rowClass: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
handleRowClick: PropTypes.func,
intlNamespace: PropTypes.string,
toggleSort: PropTypes.func,
currentSort: PropTypes.object,
paginate: PropTypes.bool,
};
Datatable.defaultProps = {
showNew: true,
showEdit: true,
showSearch: true,
paginate: false,
};
replaceComponent('Datatable', Datatable, withCurrentUser, [withStyles, baseStyles]);
/*
DatatableContents Component
*/
const datatableContentsStyles = theme => (_assign({}, baseStyles(theme), {
table: {
marginTop: theme.spacing.unit * 3,
marginBottom: theme.spacing.unit * 3,
},
denseTable: theme.utils.denseTable,
flatTable: theme.utils.flatTable,
denserTable: theme.utils.denserTable,
}));
const DatatableContents = ({
collection,
columns,
results,
loading,
loadMore,
count,
totalCount,
networkStatus,
refetch,
showEdit,
editComponent,
emptyState,
currentUser,
classes,
footerData,
dense,
queryDataRef,
rowClass,
handleRowClick,
intlNamespace,
title,
toggleSort,
currentSort,
paginate,
paginationTerms,
setPaginationTerms
}) => {
if (loading) {
return <Components.Loading/>;
} else if (!results || !results.length) {
return emptyState || null;
}
if (queryDataRef) queryDataRef(this.props);
const denseClass = dense && classes[dense + 'Table'];
// Pagination functions
const getPage = (paginationTerms) => (parseInt((paginationTerms.limit - 1) / paginationTerms.itemsPerPage));
const onChangePage = (event, page) => {
setPaginationTerms({
itemsPerPage: paginationTerms.itemsPerPage,
limit: (page + 1) * paginationTerms.itemsPerPage,
offset: page * paginationTerms.itemsPerPage
});
};
const onChangeRowsPerPage = (event) => {
let value = event.target.value;
let offset = Math.max(0, parseInt((paginationTerms.limit - paginationTerms.itemsPerPage) / value) * value);
let limit = Math.min(offset + value, totalCount);
setPaginationTerms({
itemsPerPage: value,
limit: limit,
offset: offset
});
};
return (
<React.Fragment>
{
(title)?
<Toolbar>
<Typography variant="h6" id="tableTitle">
title
</Typography>
</Toolbar>
:null
}
<Table className={classNames(classes.table, denseClass)}>
{
columns &&
<TableHead className={classes.tableHead}>
<TableRow className={classes.tableRow}>
{
_.sortBy(columns, column => column.order).map(
(column, index) =>
<Components.DatatableHeader key={index}
collection={collection}
intlNamespace={intlNamespace}
column={column}
classes={classes}
toggleSort={toggleSort}
currentSort={currentSort}
/>
)
}
{
(showEdit || editComponent) &&
<TableCell className={classes.tableCell}/>
}
</TableRow>
</TableHead>
}
{
results &&
<TableBody className={classes.tableBody}>
{
results.map(
(document, index) =>
<Components.DatatableRow collection={collection}
columns={columns}
document={document}
refetch={refetch}
key={index}
showEdit={showEdit}
editComponent={editComponent}
currentUser={currentUser}
classes={classes}
rowClass={rowClass}
handleRowClick={handleRowClick}
/>)
}
</TableBody>
}
{
footerData &&
<TableFooter className={classes.tableFooter}>
<TableRow className={classes.tableRow}>
{
_.sortBy(columns, column => column.order).map(
(column, index) =>
<TableCell key={index} className={classNames(classes.tableCell, column.footerClass)}>
{footerData[index]}
</TableCell>
)
}
{
(showEdit || editComponent) &&
<TableCell className={classes.tableCell}/>
}
</TableRow>
</TableFooter>
}
</Table>
{
paginate &&
<TablePagination
component="div"
count={totalCount}
rowsPerPage={paginationTerms.itemsPerPage}
page={getPage(paginationTerms)}
backIconButtonProps={{
'aria-label': 'Previous Page',
}}
nextIconButtonProps={{
'aria-label': 'Next Page',
}}
onChangePage={onChangePage}
onChangeRowsPerPage={onChangeRowsPerPage}
/>
}
{
!paginate && loadMore &&
<Components.LoadMore className={classes.loadMore}
count={count}
totalCount={totalCount}
loadMore={loadMore}
networkStatus={networkStatus}
/>
}
</React.Fragment>
);
};
replaceComponent('DatatableContents', DatatableContents, [withStyles, datatableContentsStyles]);
/*
DatatableHeader Component
*/
const DatatableHeader = ({ collection, intlNamespace, column, classes, toggleSort, currentSort }, { intl }) => {
const columnName = typeof column === 'string' ? column : column.name || column.label;
let formattedLabel = '';
if (collection) {
const schema = collection.simpleSchema()._schema;
/*
use either:
1. the column name translation
2. the column name label in the schema (if the column name matches a schema field)
3. the raw column name.
*/
const defaultMessage = schema[columnName] ? schema[columnName].label : Utils.camelToSpaces(columnName);
formattedLabel = typeof columnName === 'string' ?
intl.formatMessage({
id: `${collection._name}.${columnName}`,
defaultMessage: defaultMessage
}) :
'';
// if sortable is a string, use it as the name of the property to sort by. If it's just `true`, use
// column.name
const sortPropertyName = typeof column.sortable === 'string' ? column.sortable : column.name;
if (column.sortable) {
return <Components.DatatableSorter name={sortPropertyName}
label={formattedLabel}
toggleSort={toggleSort}
currentSort={currentSort}
sortable={column.sortable}
/>;
}
} else if (intlNamespace) {
formattedLabel = typeof columnName === 'string' ?
intl.formatMessage({
id: `${intlNamespace}.${columnName}`,
defaultMessage: columnName
}) :
'';
} else {
formattedLabel = intl.formatMessage({ id: columnName, defaultMessage: columnName });
}
return <TableCell
className={classNames(classes.tableHeadCell, column.headerClass)}>{formattedLabel}</TableCell>;
};
DatatableHeader.contextTypes = {
intl: intlShape,
};
replaceComponent('DatatableHeader', DatatableHeader);
/*
DatatableSorter Component
*/
const DatatableSorter = ({ name, label, toggleSort, currentSort, sortable }) =>
<TableCell className="datatable-sorter"
sortDirection={!currentSort[name] ? false : currentSort[name] === 1 ? 'asc' : 'desc'}
>
<Tooltip
title="Sort"
placement='bottom-start'
enterDelay={300}
>
<TableSortLabel
active={!currentSort[name] ? false : true}
direction={currentSort[name] === 1 ? 'desc' : 'asc'}
onClick={() => toggleSort(name)}
>
{label}
</TableSortLabel>
</Tooltip>
</TableCell>;
replaceComponent('DatatableSorter', DatatableSorter);
/*
DatatableRow Component
*/
const datatableRowStyles = theme => (_assign({}, baseStyles(theme), {
clickRow: {
cursor: 'pointer',
},
editCell: {
paddingTop: '0 !important',
paddingBottom: '0 !important',
textAlign: 'right',
},
}));
const DatatableRow = ({
collection,
columns,
document,
refetch,
showEdit,
editComponent,
currentUser,
rowClass,
handleRowClick,
classes,
}, { intl }) => {
const EditComponent = editComponent;
if (typeof rowClass === 'function') {
rowClass = rowClass(document);
}
return (
<TableRow
className={classNames('datatable-item', classes.tableRow, rowClass, handleRowClick && classes.clickRow)}
onClick={handleRowClick && (event => handleRowClick(event, document))}
hover
>
{
_.sortBy(columns, column => column.order).map(
(column, index) =>
<Components.DatatableCell key={index}
column={column}
document={document}
currentUser={currentUser}
classes={classes}
/>)
}
{
(showEdit || editComponent) &&
<TableCell className={classes.editCell}>
{
EditComponent &&
<EditComponent collection={collection} document={document} refetch={refetch}/>
}
{
showEdit &&
<Components.EditButton collection={collection}
document={document}
buttonClasses={{ button: classes.editButton }}
/>
}
</TableCell>
}
</TableRow>
);
};
replaceComponent('DatatableRow', DatatableRow, [withStyles, datatableRowStyles]);
DatatableRow.contextTypes = {
intl: intlShape
};
/*
DatatableCell Component
*/
const DatatableCell = ({ column, document, currentUser, classes }) => {
const Component = column.component ||
Components[column.componentName] ||
Components.DatatableDefaultCell;
const columnName = typeof column === 'string' ? column : column.name;
const className = typeof columnName === 'string' ?
`datatable-item-${columnName.toLowerCase()}` :
'';
const cellClass = typeof column.cellClass === 'function' ?
column.cellClass({ column, document, currentUser }) :
typeof column.cellClass === 'string' ?
column.cellClass :
null;
return (
<TableCell className={classNames(classes.tableCell, cellClass, className)}>
<Component column={column}
document={document}
currentUser={currentUser}
/>
</TableCell>
);
};
replaceComponent('DatatableCell', DatatableCell);
/*
DatatableDefaultCell Component
*/
const DatatableDefaultCell = ({ column, document }) =>
<div>
{
typeof column === 'string'
?
getFieldValue(document[column])
:
getFieldValue(document[column.name])
}
</div>;
replaceComponent('DatatableDefaultCell', DatatableDefaultCell);

View file

@ -0,0 +1,100 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Components, registerComponent } from 'meteor/vulcan:core';
import { intlShape } from 'meteor/vulcan:i18n';
import EditIcon from 'mdi-material-ui/Pencil';
const EditButton = (
{
collection,
document,
color = 'default',
variant,
triggerClasses,
buttonClasses,
showRemove,
...props
},
{ intl }
) => (
<Components.ModalTrigger
classes={triggerClasses}
component={
<Components.TooltipIconButton
titleId="datatable.edit"
icon={<EditIcon />}
color={color}
variant={variant}
classes={buttonClasses}
/>
}
>
<Components.EditForm
collection={collection}
document={document}
showRemove={showRemove}
{...props}
/>
</Components.ModalTrigger>
);
EditButton.propTypes = {
collection: PropTypes.object.isRequired,
document: PropTypes.object.isRequired,
color: PropTypes.oneOf(['default', 'inherit', 'primary', 'secondary']),
variant: PropTypes.string,
triggerClasses: PropTypes.object,
buttonClasses: PropTypes.object,
showRemove: PropTypes.bool
};
EditButton.contextTypes = {
intl: intlShape
};
EditButton.displayName = 'EditButton';
registerComponent('EditButton', EditButton);
/*
EditForm Component
*/
const EditForm = ({
collection,
document,
closeModal,
options,
successCallback,
removeSuccessCallback,
showRemove,
...props
}) => {
const success = successCallback
? () => {
successCallback();
closeModal();
}
: closeModal;
const remove = removeSuccessCallback
? () => {
removeSuccessCallback();
closeModal();
}
: closeModal;
return (
<Components.SmartForm
{...props}
collection={collection}
documentId={document && document._id}
showRemove={showRemove ? true : showRemove}
successCallback={success}
removeSuccessCallback={remove}
/>
);
};
registerComponent('EditForm', EditForm);

View file

@ -0,0 +1,9 @@
import React from 'react';
import { replaceComponent } from 'meteor/vulcan:core';
import CircularProgress from '@material-ui/core/CircularProgress';
function Loading(props) {
return <CircularProgress {...props} />;
}
replaceComponent('Loading', Loading);

View file

@ -0,0 +1,159 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { intlShape } from 'meteor/vulcan:i18n';
import { registerComponent, Components } from 'meteor/vulcan:core';
import withStyles from '@material-ui/core/styles/withStyles';
import Dialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogContent from '@material-ui/core/DialogContent';
import Button from '@material-ui/core/Button';
import Tooltip from '@material-ui/core/Tooltip';
import Close from 'mdi-material-ui/Close';
import classNames from 'classnames';
const styles = theme => ({
root: {
display: 'inline-block',
},
button: {},
anchor: {},
dialog: {},
dialogPaper: {
overflowY: 'visible',
},
dialogTitle: {
padding: theme.spacing.unit * 4,
},
dialogContent: {
paddingTop: '4px',
},
closeButton: {
position: 'absolute',
right: theme.spacing.unit,
top: theme.spacing.unit,
}
});
class ModalTrigger extends PureComponent {
constructor (props) {
super(props);
this.state = { modalIsOpen: false };
}
componentDidMount() {
if (this.props.action) {
this.props.action({
openModal: this.openModal,
closeModal: this.closeModal,
});
}
}
openModal = () => {
this.setState({ modalIsOpen: true });
};
closeModal = () => {
this.setState({ modalIsOpen: false });
};
render () {
const {
className,
dialogClassName,
dialogOverflow,
labelId,
component,
titleId,
type,
children,
classes,
} = this.props;
const intl = this.context.intl;
const label = labelId ? intl.formatMessage({ id: labelId }) : this.props.label;
const title = titleId ? intl.formatMessage({ id: titleId }) : this.props.title;
const overflowClass = dialogOverflow && classes.dialogOverflow;
const triggerComponent = component
?
React.cloneElement(component, { onClick: this.openModal })
:
type === 'button'
?
<Button className={classes.button} variant="contained" onClick={this.openModal}>{label}</Button>
:
<a className={classes.anchor} href="#" onClick={this.openModal}>{label}</a>;
const childrenComponent = typeof children.type === 'function' ?
React.cloneElement(children, { closeModal: this.closeModal }) :
children;
return (
<span className={classNames('modal-trigger', classes.root, className)}>
{triggerComponent}
<Dialog className={classNames(dialogClassName)}
open={this.state.modalIsOpen}
onClose={this.closeModal}
fullWidth={true}
classes={{ paper: classes.paper }}
>
<DialogTitle className={classes.dialogTitle}>
{title}
<Components.Button iconButton aria-label="Close" className={classes.closeButton} onClick={this.closeModal}>
<Tooltip title={intl.formatMessage({ id: 'modal.close' })}>
<Close />
</Tooltip>
</Components.Button>
</DialogTitle>
<DialogContent className={classes.dialogContent}>
<Components.ErrorCatcher>
{childrenComponent}
</Components.ErrorCatcher>
</DialogContent>
</Dialog>
</span>
);
}
}
ModalTrigger.propTypes = {
/**
* Callback fired when the component mounts.
* This is useful when you want to trigger an action programmatically.
* It supports `openModal()` and `closeModal()`.
*
* @param {object} actions This object contains all possible actions
* that can be triggered programmatically.
*/
action: PropTypes.func,
className: PropTypes.string,
dialogClassName: PropTypes.string,
dialogOverflow: PropTypes.bool,
label: PropTypes.string,
labelId: PropTypes.string,
component: PropTypes.object,
title: PropTypes.node,
titleId: PropTypes.string,
type: PropTypes.oneOf(['link', 'button']),
children: PropTypes.node,
classes: PropTypes.object,
};
ModalTrigger.contextTypes = {
intl: intlShape,
};
registerComponent('ModalTrigger', ModalTrigger, [withStyles, styles]);

View file

@ -0,0 +1,44 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Components, replaceComponent } from 'meteor/vulcan:core';
import { intlShape } from 'meteor/vulcan:i18n';
import AddIcon from 'mdi-material-ui/Plus';
const NewButton = ({
className,
collection,
color = 'default',
variant,
}, { intl }) => (
<Components.ModalTrigger
className={className}
component={<Components.TooltipIconButton titleId="datatable.new"
icon={<AddIcon/>}
color={color}
variant={variant}
/>}
>
<Components.EditForm collection={collection}/>
</Components.ModalTrigger>
);
NewButton.propTypes = {
className: PropTypes.string,
collection: PropTypes.object.isRequired,
color: PropTypes.oneOf(['default', 'inherit', 'primary', 'secondary']),
variant: PropTypes.string,
};
NewButton.contextTypes = {
intl: intlShape
};
NewButton.displayName = 'NewButton';
replaceComponent('NewButton', NewButton);

View file

@ -0,0 +1,129 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { intlShape } from 'meteor/vulcan:i18n';
import { Components, registerComponent, instantiateComponent } from 'meteor/vulcan:core';
import withStyles from '@material-ui/core/styles/withStyles';
import _omit from 'lodash/omit';
import classNames from 'classnames';
const styles = theme => ({
formInput: {
position: 'relative',
marginBottom: theme.spacing.unit * 3,
},
halfWidthLeft: {
display: 'inline-block',
width: '48%',
verticalAlign: 'top',
marginRight: '4%',
},
halfWidthRight: {
display: 'inline-block',
width: '48%',
verticalAlign: 'top',
},
thirdWidthLeft: {
display: 'inline-block',
width: '31%',
verticalAlign: 'top',
marginRight: '3.5%',
},
thirdWidthRight: {
display: 'inline-block',
width: '31%',
verticalAlign: 'top',
},
hidden: {
display: 'none',
},
});
class FormComponentInner extends PureComponent {
getProperties = () => {
return _omit(this.props, 'classes');
};
render () {
const {
classes,
inputClassName,
name,
input,
hidden,
beforeComponent,
afterComponent,
formInput,
intlInput,
nestedInput,
formComponents,
} = this.props;
const FormComponents = formComponents;
const inputClass = classNames(
classes.formInput,
hidden && classes.hidden,
inputClassName && classes[inputClassName],
`input-${name}`,
`form-component-${input || 'default'}`
);
const properties = this.getProperties();
const FormInput = formInput;
if (intlInput) {
return <Components.FormIntl {...properties} />;
} else if (nestedInput){
return <Components.FormNested {...properties} />;
} else {
return (
<div className={inputClass}>
{instantiateComponent(beforeComponent, properties)}
<FormInput {...properties}/>
{instantiateComponent(afterComponent, properties)}
</div>
);
}
}
}
FormComponentInner.contextTypes = {
intl: intlShape,
};
FormComponentInner.propTypes = {
classes: PropTypes.object.isRequired,
inputClassName: PropTypes.string,
name: PropTypes.string.isRequired,
input: PropTypes.any,
beforeComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
afterComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
errors: PropTypes.array.isRequired,
help: PropTypes.node,
onChange: PropTypes.func.isRequired,
showCharsRemaining: PropTypes.bool.isRequired,
charsRemaining: PropTypes.number,
charsCount: PropTypes.number,
max: PropTypes.oneOfType([PropTypes.number, PropTypes.instanceOf(Date)]),
formInput: PropTypes.func.isRequired,
};
FormComponentInner.displayName = 'FormComponentInner';
registerComponent('FormComponentInner', FormComponentInner, [withStyles, styles]);

View file

@ -0,0 +1,51 @@
import React from 'react';
import PropTypes from 'prop-types';
import { replaceComponent, Components } from 'meteor/vulcan:core';
import { FormattedMessage } from 'meteor/vulcan:i18n';
import Snackbar from '@material-ui/core/Snackbar';
import withStyles from '@material-ui/core/styles/withStyles';
import classNames from 'classnames';
const styles = theme => ({
root: {
position: 'relative',
boxShadow: 'none',
marginBottom: theme.spacing.unit * 2
},
list: {
marginBottom: 0
},
error: { '& > div': { backgroundColor: theme.palette.error[500] } },
danger: { '& > div': { backgroundColor: theme.palette.error[500] } },
warning: { '& > div': { backgroundColor: theme.palette.error[500] } }
});
const FormErrors = ({ errors, classes }) => {
const messageNode = (
<ul className={classes.list}>
{errors.map((error, index) => (
<li key={index}>
<Components.FormError error={error} />
</li>
))}
</ul>
);
return (
<div>
{!!errors.length && (
<Snackbar
open={true}
className={classNames('flash-message', classes.root , classes.danger)}
message={messageNode}
/>
)}
</div>
);
};
replaceComponent('FormErrors', FormErrors, [withStyles, styles]);

View file

@ -0,0 +1,95 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import {
Components,
registerComponent,
instantiateComponent,
} from 'meteor/vulcan:core';
import withStyles from '@material-ui/core/styles/withStyles';
import Users from 'meteor/vulcan:users';
import classNames from 'classnames';
const styles = theme => ({
root: {
minWidth: '320px'
},
});
class FormGroupNone extends PureComponent {
render () {
const {
name,
hidden,
classes,
currentUser,
} = this.props;
if (this.isAdmin && !Users.isAdmin(currentUser)) {
return null;
}
if (typeof hidden === 'function' ? hidden({ ...this.props }) : hidden) {
return null;
}
//do not display if no fields, no startComponent and no endComponent
if (!this.props.startComponent && !this.props.endComponent && !this.props.fields.length) {
return null;
}
const anchorName = name.split('.').length > 1 ? name.split('.')[1] : name;
const FormComponents = this.props.formComponents;
return (
<div className={classes.root}>
<a name={anchorName}/>
{instantiateComponent(this.props.startComponent)}
{this.props.fields.map(field => (
<FormComponents.FormComponent
key={field.name}
disabled={this.props.disabled}
{...field}
errors={this.props.errors}
throwError={this.props.throwError}
currentValues={this.props.currentValues}
updateCurrentValues={this.props.updateCurrentValues}
deletedValues={this.props.deletedValues}
addToDeletedValues={this.props.addToDeletedValues}
clearFieldErrors={this.props.clearFieldErrors}
formType={this.props.formType}
currentUser={this.props.currentUser}
formComponents={FormComponents}
/>
))}
{instantiateComponent(this.props.endComponent)}
</div>
);
}
}
FormGroupNone.propTypes = {
name: PropTypes.string,
order: PropTypes.number,
hidden: PropTypes.bool,
fields: PropTypes.array,
updateCurrentValues: PropTypes.func,
startComponent: PropTypes.node,
endComponent: PropTypes.node,
currentUser: PropTypes.object,
};
registerComponent('FormGroupNone', FormGroupNone, [withStyles, styles]);

View file

@ -0,0 +1,198 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { Components, registerComponent, instantiateComponent } from 'meteor/vulcan:core';
import Users from 'meteor/vulcan:users';
import withStyles from '@material-ui/core/styles/withStyles';
import Collapse from '@material-ui/core/Collapse';
import Divider from '@material-ui/core/Divider';
import Typography from '@material-ui/core/Typography';
import ExpandLessIcon from 'mdi-material-ui/ChevronUp';
import ExpandMoreIcon from 'mdi-material-ui/ChevronDown';
import classNames from 'classnames';
const styles = theme => ({
root: {
minWidth: '320px',
},
divider: {
marginLeft: theme.spacing.unit * -3,
marginRight: theme.spacing.unit * -3,
},
subtitle1: {
marginTop: theme.spacing.unit * 5,
position: 'relative',
},
collapsible: {
cursor: 'pointer',
},
typography: {
display: 'flex',
alignItems: 'center',
'& > div': {
display: 'flex',
alignItems: 'center',
},
'& > div:first-child': {
...theme.typography.subtitle1,
},
paddingTop: theme.spacing.unit,
paddingBottom: theme.spacing.unit,
},
toggle: {
color: theme.palette.action.active,
},
entered: {
overflow: 'visible',
},
});
class FormGroupWithLine extends PureComponent {
constructor (props) {
super(props);
this.isAdmin = props.name === 'admin';
this.state = {
collapsed: props.startCollapsed || this.isAdmin,
};
}
toggle = () => {
const collapsible = this.props.collapsible || this.isAdmin;
if (!collapsible) return;
this.setState({
collapsed: !this.state.collapsed
});
};
renderHeading = () => {
const { classes } = this.props;
const collapsible = this.props.collapsible || this.isAdmin;
return (
<div className={classNames(classes.subtitle1, collapsible && classes.collapsible)} onClick={this.toggle}>
<Divider className={classes.divider}/>
<Typography className={classes.typography} variant="subtitle1" gutterBottom>
<div>
{this.props.label}
</div>
{
collapsible &&
<div className={classes.toggle}>
{
this.state.collapsed
?
<ExpandMoreIcon/>
:
<ExpandLessIcon/>
}
</div>
}
</Typography>
</div>
);
};
// if at least one of the fields in the group has an error, the group as a whole has an error
hasErrors = () => _.some(this.props.fields, field => {
return !!this.props.errors.filter(error => error.path === field.path).length;
});
render () {
const {
name,
hidden,
classes,
currentUser,
} = this.props;
if (this.isAdmin && !Users.isAdmin(currentUser)) {
return null;
}
if (typeof hidden === 'function' ? hidden({ ...this.props }) : hidden) {
return null;
}
//do not display if no fields, no startComponent and no endComponent
if (!this.props.startComponent && !this.props.endComponent && !this.props.fields.length) {
return null;
}
const anchorName = name.split('.').length > 1 ? name.split('.')[1] : name;
const collapseIn = !this.state.collapsed || this.hasErrors();
const FormComponents = this.props.formComponents;
return (
<div className={classes.root}>
<a name={anchorName}/>
{
this.props.name === 'default'
?
null
:
this.renderHeading()
}
<Collapse classes={{ entered: classes.entered }} in={collapseIn}>
{instantiateComponent(this.props.startComponent)}
{this.props.fields.map(field => (
<FormComponents.FormComponent
key={field.name}
disabled={this.props.disabled}
{...field}
errors={this.props.errors}
throwError={this.props.throwError}
currentValues={this.props.currentValues}
updateCurrentValues={this.props.updateCurrentValues}
deletedValues={this.props.deletedValues}
addToDeletedValues={this.props.addToDeletedValues}
clearFieldErrors={this.props.clearFieldErrors}
formType={this.props.formType}
currentUser={this.props.currentUser}
formComponents={FormComponents}
/>
))}
{instantiateComponent(this.props.endComponent)}
</Collapse>
</div>
);
}
}
FormGroupWithLine.propTypes = {
name: PropTypes.string,
label: PropTypes.string,
order: PropTypes.number,
fields: PropTypes.array,
collapsible: PropTypes.bool,
startCollapsed: PropTypes.bool,
updateCurrentValues: PropTypes.func,
startComponent: PropTypes.node,
endComponent: PropTypes.node,
currentUser: PropTypes.object,
};
registerComponent('FormGroupWithLine', FormGroupWithLine, [withStyles, styles]);

View file

@ -0,0 +1,13 @@
import React from 'react';
import { replaceComponent } from 'meteor/vulcan:core';
import Delete from 'mdi-material-ui/Delete';
import Plus from 'mdi-material-ui/Plus';
const IconRemove = () => <Delete/>;
replaceComponent('IconRemove', IconRemove);
const IconAdd = () => <Plus/>;
replaceComponent('IconAdd', IconAdd);

View file

@ -0,0 +1,24 @@
import React from 'react';
import PropTypes from 'prop-types';
import { replaceComponent } from 'meteor/vulcan:core';
import Grid from '@material-ui/core/Grid';
import Typography from '@material-ui/core/Typography';
const FormNestedArrayLayout = ({ hasErrors, label, content }) => (
<div>
<Typography component="label" variant="caption" style={{ fontSize: 16 }}>
{label}
</Typography>
<div>{content}</div>
</div>
);
FormNestedArrayLayout.propTypes = {
hasErrors: PropTypes.bool,
label: PropTypes.node,
content: PropTypes.node,
};
replaceComponent({
name: 'FormNestedArrayLayout',
component: FormNestedArrayLayout,
});

View file

@ -0,0 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import { replaceComponent } from 'meteor/vulcan:core';
// import { FormattedMessage } from 'meteor/vulcan:i18n';
import withStyles from '@material-ui/core/styles/withStyles';
import Divider from '@material-ui/core/Divider';
const styles = theme => ({
divider: {
// marginLeft: -24,
// marginRight: -24,
marginTop: 16,
marginBottom: 23,
},
});
const FormNestedDivider = ({ classes, label, addItem }) => <Divider className={classes.divider} />;
FormNestedDivider.propTypes = {
classes: PropTypes.object.isRequired,
label: PropTypes.string,
addItem: PropTypes.func,
};
replaceComponent('FormNestedDivider', FormNestedDivider, [withStyles, styles]);

View file

@ -0,0 +1,20 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Components, registerComponent } from 'meteor/vulcan:core';
import { FormattedMessage } from 'meteor/vulcan:i18n';
import Grid from '@material-ui/core/Grid';
const FormNestedFoot = ({ label, addItem }) => (
<Grid container spacing={0} justify="flex-end">
<Components.Button color="primary" iconButton onClick={addItem}>
<Components.IconAdd/>
</Components.Button>
</Grid>
);
FormNestedFoot.propTypes = {
label: PropTypes.string,
addItem: PropTypes.func,
};
registerComponent('FormNestedFoot', FormNestedFoot);

View file

@ -0,0 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import { replaceComponent } from 'meteor/vulcan:core';
import { FormattedMessage } from 'meteor/vulcan:i18n';
const FormNestedHead = ({ label, addItem }) => <span/>;
FormNestedHead.propTypes = {
label: PropTypes.string,
addItem: PropTypes.func,
};
replaceComponent('FormNestedHead', FormNestedHead);

View file

@ -0,0 +1,136 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Components, replaceComponent } from 'meteor/vulcan:core';
import { intlShape } from 'meteor/vulcan:i18n';
import withStyles from '@material-ui/core/styles/withStyles';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import DeleteIcon from 'mdi-material-ui/Delete';
import Tooltip from '@material-ui/core/Tooltip';
import { FormattedMessage } from 'meteor/vulcan:i18n';
import classNames from 'classnames';
const styles = theme => ({
root: {
textAlign: 'center',
marginTop: theme.spacing.unit * 4,
},
button: {
margin: theme.spacing.unit,
},
delete: {
float: 'left',
},
tooltip: {
margin: 3,
}
});
const FormSubmit = ({
submitLabel,
cancelLabel,
cancelCallback,
revertLabel,
revertCallback,
document,
deleteDocument,
collectionName,
classes
}, {
intl,
isChanged,
clearForm,
}) => {
if (typeof isChanged !== 'function') {
isChanged = () => true;
}
return (
<div className={classes.root}>
{
deleteDocument
?
<Tooltip id={`tooltip-delete-${collectionName}`}
classes={{ tooltip: classNames('delete-button', classes.tooltip) }}
title={intl.formatMessage({ id: 'forms.delete' })}
placement="bottom">
<IconButton onClick={deleteDocument} className={classes.delete}>
<DeleteIcon/>
</IconButton>
</Tooltip>
:
null
}
{
cancelCallback
?
<Button variant="contained"
className={classNames('cancel-button', classes.button)}
onClick={(event) => {
event.preventDefault();
cancelCallback(document);
}}>
{cancelLabel ? cancelLabel : <FormattedMessage id="forms.cancel"/>}
</Button>
:
null
}
{
revertCallback
?
<Button variant="contained"
className={classNames('revert-button', classes.button)}
disabled={!isChanged()}
onClick={(event) => {
event.preventDefault();
clearForm({ clearErrors: true, clearCurrentValues: true, clearDeletedValues: true });
revertCallback(document);
}}
>
{revertLabel ? revertLabel : <FormattedMessage id="forms.revert"/>}
</Button>
:
null
}
<Button variant="contained"
type="submit"
color="secondary"
className={classNames('submit-button', classes.button)}
disabled={!isChanged()}
>
{submitLabel ? submitLabel : <FormattedMessage id="forms.submit"/>}
</Button>
</div>
);
};
FormSubmit.propTypes = {
submitLabel: PropTypes.node,
cancelLabel: PropTypes.node,
revertLabel: PropTypes.node,
cancelCallback: PropTypes.func,
revertCallback: PropTypes.func,
document: PropTypes.object,
deleteDocument: PropTypes.func,
collectionName: PropTypes.string,
classes: PropTypes.object,
};
FormSubmit.contextTypes = {
intl: intlShape,
isChanged: PropTypes.func,
clearForm: PropTypes.func,
};
replaceComponent('FormSubmit', FormSubmit, [withStyles, styles]);

View file

@ -0,0 +1,84 @@
import React from 'react';
import PropTypes from 'prop-types';
import { instantiateComponent } from 'meteor/vulcan:core';
import withStyles from '@material-ui/core/styles/withStyles';
import InputAdornment from '@material-ui/core/InputAdornment';
import IconButton from '@material-ui/core/IconButton';
import CloseIcon from 'mdi-material-ui/CloseCircle';
import classNames from 'classnames';
export const styles = theme => ({
inputAdornment: {
whiteSpace: 'nowrap',
marginTop: '0 !important',
'& > *': {
verticalAlign: 'bottom',
},
'& > svg': {
color: theme.palette.common.darkBlack,
},
'& > * + *': {
marginLeft: 8,
}
},
clearButton: {
opacity: 0,
'& svg': {
width: 20,
height: 20,
},
marginRight: -12,
marginLeft: -4,
'&:first-child': {
marginLeft: -12,
},
transition: theme.transitions.create('opacity', {
duration: theme.transitions.duration.short,
}),
},
urlButton: {
verticalAlign: 'bottom',
width: 24,
height: 24,
fontSize: 20,
}
});
const EndAdornment = (props) => {
const { classes, value, addonAfter, changeValue, hideClear, disabled } = props;
if (!addonAfter && (!changeValue || hideClear || disabled)) return null;
const hasValue = !!value || value === 0;
const clearButton = changeValue && !hideClear && !disabled &&
<IconButton className={classNames('clear-button', classes.clearButton, hasValue && 'clear-enabled')}
onClick={event => {
event.preventDefault();
changeValue(null);
}}
tabIndex="-1"
>
<CloseIcon/>
</IconButton>;
return (
<InputAdornment classes={{ root: classes.inputAdornment }} position="end">
{instantiateComponent(addonAfter)}
{clearButton}
</InputAdornment>
);
};
EndAdornment.propTypes = {
classes: PropTypes.object.isRequired,
value: PropTypes.any,
changeValue: PropTypes.func,
hideClear: PropTypes.bool,
addonAfter: PropTypes.oneOfType([PropTypes.string, PropTypes.node, PropTypes.func]),
};
export default withStyles(styles)(EndAdornment);

View file

@ -0,0 +1,150 @@
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import ComponentMixin from './mixins/component';
import withStyles from '@material-ui/core/styles/withStyles';
import FormGroup from '@material-ui/core/FormGroup';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import MuiFormControl from './MuiFormControl';
import MuiFormHelper from './MuiFormHelper';
import Checkbox from '@material-ui/core/Checkbox';
import Switch from '@material-ui/core/Switch';
import classNames from 'classnames';
const styles = theme => ({
group: {
marginTop: '8px',
},
twoColumn: {
display: 'block',
[theme.breakpoints.down('md')]: {
'& > label': {
marginRight: theme.spacing.unit * 5,
},
},
[theme.breakpoints.up('md')]: {
'& > label': {
width: '49%',
},
},
},
threeColumn: {
display: 'block',
[theme.breakpoints.down('xs')]: {
'& > label': {
marginRight: theme.spacing.unit * 5,
},
},
[theme.breakpoints.up('xs')]: {
'& > label': {
width: '49%',
},
},
[theme.breakpoints.up('md')]: {
'& > label': {
width: '32%',
},
},
},
});
const MuiCheckboxGroup = createReactClass({
mixins: [ComponentMixin],
propTypes: {
name: PropTypes.string.isRequired,
options: PropTypes.array.isRequired,
classes: PropTypes.object.isRequired,
variant: PropTypes.oneOf(['checkbox', 'switch']),
},
componentDidMount: function () {
if (this.props.refFunction) {
this.props.refFunction(this);
}
},
getDefaultProps: function () {
return {
label: '',
help: null,
variant: 'checkbox',
};
},
changeCheckbox: function () {
const value = [];
this.props.options.forEach(function (option, key) {
if (this[this.props.name + '-' + option.value].checked) {
value.push(option.value);
}
}.bind(this));
this.props.onChange(value);
},
validate: function () {
if (this.props.onBlur) {
this.props.onBlur();
}
return true;
},
renderElement: function () {
const controls = this.props.options.map((checkbox, key) => {
let value = checkbox.value;
let checked = (this.props.value.indexOf(value) !== -1);
let disabled = checkbox.disabled || this.props.disabled;
const Component = this.props.variant === 'switch' ? Switch : Checkbox;
return (
<FormControlLabel
key={key}
control={
<Component
inputRef={(c) => this[this.props.name + '-' + value] = c}
checked={checked}
onChange={this.changeCheckbox}
value={value}
disabled={disabled}
/>
}
label={checkbox.label}
/>
);
});
const maxLength = this.props.options.reduce((max, option) =>
option.label.length > max ? option.label.length : max, 0);
const columnClass = maxLength < 20 ? 'threeColumn' : maxLength < 30 ? 'twoColumn' : '';
return (
<FormGroup className={classNames(this.props.classes.group, this.props.classes[columnClass])}>
{controls}
</FormGroup>
);
},
render: function () {
if (this.props.layout === 'elementOnly') {
return (
<div>{this.renderElement()}</div>
);
}
return (
<MuiFormControl{...this.getFormControlProperties()} fakeLabel={true}>
{this.renderElement()}
<MuiFormHelper {...this.getFormHelperProperties()}/>
</MuiFormControl>
);
}
});
export default withStyles(styles)(MuiCheckboxGroup);

View file

@ -0,0 +1,91 @@
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import InputLabel from '@material-ui/core/InputLabel';
import FormControl from '@material-ui/core/FormControl';
import FormLabel from '@material-ui/core/FormLabel';
//noinspection JSUnusedGlobalSymbols
const MuiFormControl = createReactClass({
propTypes: {
label: PropTypes.node,
children: PropTypes.node,
required: PropTypes.bool,
hasErrors: PropTypes.bool,
fakeLabel: PropTypes.bool,
hideLabel: PropTypes.bool,
layout: PropTypes.oneOf(['horizontal', 'vertical', 'elementOnly']),
htmlFor: PropTypes.string
},
getDefaultProps: function () {
return {
label: '',
required: false,
hasErrors: false,
fakeLabel: false,
hideLabel: false,
};
},
renderRequiredSymbol: function () {
if (this.props.required === false) {
return null;
}
return (
<span className="required-symbol"> *</span>
);
},
renderLabel: function () {
if (this.props.layout === 'elementOnly' || this.props.hideLabel) {
return null;
}
if (this.props.fakeLabel) {
return (
<FormLabel className="control-label legend"
component="legend"
data-required={this.props.required}
>
{this.props.label}
{this.renderRequiredSymbol()}
</FormLabel>
);
}
const shrink = ['date', 'time', 'datetime'].includes(this.props.inputType) ? true : undefined;
return (
<InputLabel className="control-label"
data-required={this.props.required}
htmlFor={this.props.htmlFor}
shrink={shrink}
>
{this.props.label}
{this.renderRequiredSymbol()}
</InputLabel>
);
},
render: function () {
const { layout, className, children, hasErrors } = this.props;
if (layout === 'elementOnly') {
return <span>{children}</span>;
}
return (
<FormControl component="fieldset" error={hasErrors} fullWidth={true} className={className}>
{this.renderLabel()}
{children}
</FormControl>
);
}
});
export default MuiFormControl;

View file

@ -0,0 +1,75 @@
import React from 'react';
import PropTypes from 'prop-types';
import { instantiateComponent, Components } from 'meteor/vulcan:core';
import withStyles from '@material-ui/core/styles/withStyles';
import FormHelperText from '@material-ui/core/FormHelperText';
import { FormattedMessage } from 'meteor/vulcan:i18n';
import classNames from 'classnames';
export const styles = theme => ({
error: {
color: theme.palette.error.main,
},
formHelperText: {
display: 'flex',
'& :first-child': {
flexGrow: 1,
}
},
});
const MuiFormHelper = (props) => {
const {
classes,
help,
errors,
hasErrors,
showCharsRemaining,
charsRemaining,
charsCount,
max,
} = props;
if (!help && !hasErrors && !showCharsRemaining) {
return null;
}
const errorMessage = hasErrors &&
<Components.FormError error={errors[0]} />;
return (
<FormHelperText className={classes.formHelperText} error={hasErrors}>
<span>
{
hasErrors ? errorMessage : help
}
</span>
{
showCharsRemaining &&
<span className={charsRemaining < 0 ? classes.error : null}>
{charsCount} / {max}
</span>
}
</FormHelperText>
);
};
MuiFormHelper.propTypes = {
classes: PropTypes.object.isRequired,
value: PropTypes.any,
changeValue: PropTypes.func,
addonAfter: PropTypes.oneOfType([PropTypes.string, PropTypes.node, PropTypes.func]),
};
export default withStyles(styles)(MuiFormHelper);

View file

@ -0,0 +1,138 @@
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import withStyles from '@material-ui/core/styles/withStyles';
import ComponentMixin from './mixins/component';
import MuiFormControl from './MuiFormControl';
import MuiFormHelper from './MuiFormHelper';
import Input from '@material-ui/core/Input';
import StartAdornment, { hideStartAdornment, fixUrl } from './StartAdornment';
import EndAdornment from './EndAdornment';
export const styles = theme => ({
inputRoot: {
'& .clear-enabled': { opacity: 0 },
'&:hover .clear-enabled': { opacity: 0.54 },
},
inputFocused: {
'& .clear-enabled': { opacity: 0.54 }
},
});
//noinspection JSUnusedGlobalSymbols
const MuiInput = createReactClass({
element: null,
mixins: [ComponentMixin],
displayName: 'MuiInput',
propTypes: {
type: PropTypes.oneOf([
'color',
'date',
'datetime',
'datetime-local',
'email',
'hidden',
'month',
'number',
'password',
'range',
'search',
'tel',
'text',
'time',
'url',
'week'
]),
errors: PropTypes.array,
placeholder: PropTypes.string,
formatValue: PropTypes.func,
scrubValue: PropTypes.func,
hideClear: PropTypes.bool,
},
getDefaultProps: function () {
return {
type: 'text',
};
},
handleChange: function (event) {
let value = event.target.value;
if (this.props.scrubValue) {
value = this.props.scrubValue(value);
}
this.changeValue(value);
},
changeValue: function (value) {
this.props.onChange(value);
},
handleBlur: function (event) {
const { type, value } = this.props;
if (type === 'url' && !!value && value !== fixUrl(value)) {
this.changeValue(fixUrl(value));
}
},
render: function () {
const startAdornment = hideStartAdornment(this.props) ? null :
<StartAdornment {...this.props}
classes={null}
changeValue={this.changeValue}
/>;
const endAdornment =
<EndAdornment {...this.props}
classes={null}
changeValue={this.changeValue}
/>;
let element = this.renderElement(startAdornment, endAdornment);
if (this.props.layout === 'elementOnly' || this.props.type === 'hidden') {
return element;
}
return (
<MuiFormControl {...this.getFormControlProperties()} htmlFor={this.getId()}>
{element}
<MuiFormHelper {...this.getFormHelperProperties()}/>
</MuiFormControl>
);
},
renderElement: function (startAdornment, endAdornment) {
const { classes, disabled, autoFocus, formatValue } = this.props;
const value = formatValue ? formatValue(this.props.value) : this.props.value;
const options = this.props.options || {};
return (
<Input
ref={c => (this.element = c)}
{...this.cleanProps(this.props)}
id={this.getId()}
value={value}
onChange={this.handleChange}
onBlur={this.handleBlur}
disabled={disabled}
rows={options.rows || this.props.rows}
autoFocus={options.autoFocus || autoFocus}
startAdornment={startAdornment}
endAdornment={endAdornment}
placeholder={this.props.placeholder}
classes={{ root: classes.inputRoot, focused: classes.inputFocused }}
/>
);
},
});
export default withStyles(styles)(MuiInput);

Some files were not shown because too many files have changed in this diff Show more