Merge branch 'devel'

This commit is contained in:
SachaG 2017-12-27 09:58:07 +09:00
commit 867495b8ec
138 changed files with 3053 additions and 666 deletions

View file

@ -10,7 +10,7 @@ indent_brace_style = 1TBS
indent_size = 2
indent_style = space
insert_final_newline = true
max_line_length = 80
max_line_length = 120
quote_type = auto
spaces_around_operators = true
trim_trailing_whitespace = true

View file

@ -50,10 +50,15 @@
"meteor": true,
"node": true
},
"ecmaFeatures": {
"modules": true,
"jsx": true
},
"plugins": [
"babel",
"meteor",
"react"
"react",
"prettier"
],
"settings": {
"import/resolver": "meteor"

7
.meteor/.id Normal file
View file

@ -0,0 +1,7 @@
# This file contains a token that is unique to your project.
# Check it into your repository along with the rest of this directory.
# It can be used for purposes such as:
# - ensuring you don't accidentally deploy one app on top of another
# - providing package authors with aggregated statistics
1txv9r51kxht481ysl8bb

View file

@ -1 +1 @@
METEOR@1.5.2.2
METEOR@1.6

15
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}"
}
]
}

549
package-lock.json generated
View file

@ -1,9 +1,269 @@
{
"name": "Vulcan",
"version": "1.2.0",
"version": "1.8.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"accepts": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz",
"integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=",
"requires": {
"mime-types": "2.1.17",
"negotiator": "0.6.1"
}
},
"ajv": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.4.0.tgz",
"integrity": "sha1-MtHPCNvIDEMvQm8S4QslEfa0ZHQ=",
"requires": {
"co": "4.6.0",
"fast-deep-equal": "1.0.0",
"fast-json-stable-stringify": "2.0.0",
"json-schema-traverse": "0.3.1"
}
},
"apollo-cache-control": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/apollo-cache-control/-/apollo-cache-control-0.0.7.tgz",
"integrity": "sha512-DoMTr3uTC5Cx9ukSO63wlzHD15C37FwZuoOZEu+m/UTzVFKQ4PnlBKzwZ0H2+iIwcdSulV0xte6Z3wBe9lHAOA==",
"requires": {
"graphql-extensions": "0.0.5"
}
},
"apollo-engine": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/apollo-engine/-/apollo-engine-0.5.4.tgz",
"integrity": "sha512-91yqiM1fB33fjvcsIBICy8BUHh2cG9FAIrteCh9QaL7UwJ6aQMV5DSfjNhgP95DEZcPMggKQGLhbW156A7G0mg==",
"requires": {
"apollo-engine-binary-darwin": "0.2017.11-84-gb299b9188",
"apollo-engine-binary-linux": "0.2017.11-84-gb299b9188",
"apollo-engine-binary-windows": "0.2017.11-84-gb299b9188",
"request": "2.83.0",
"stream-line-wrapper": "0.1.1"
}
},
"apollo-engine-binary-darwin": {
"version": "0.2017.11-84-gb299b9188",
"resolved": "https://registry.npmjs.org/apollo-engine-binary-darwin/-/apollo-engine-binary-darwin-0.2017.11-84-gb299b9188.tgz",
"integrity": "sha512-8zKIFo6ldSwT1npHU4gjHMDEJQuN/CG3MCnx5xY5MGSSkqlqNZZ8njYgXe4qLEjewLMwRTXapcnCw7E2+H1RYQ==",
"optional": true
},
"apollo-engine-binary-linux": {
"version": "0.2017.11-84-gb299b9188",
"resolved": "https://registry.npmjs.org/apollo-engine-binary-linux/-/apollo-engine-binary-linux-0.2017.11-84-gb299b9188.tgz",
"integrity": "sha512-Y+DYYoR24yi73+Kt03Nr7IXNoMJw6faEgdUpysMdnkIdmqaFfcKj3KH0auzVBhPyVcJo+iRTKqXdnMzjnQxrsg==",
"optional": true
},
"apollo-engine-binary-windows": {
"version": "0.2017.11-84-gb299b9188",
"resolved": "https://registry.npmjs.org/apollo-engine-binary-windows/-/apollo-engine-binary-windows-0.2017.11-84-gb299b9188.tgz",
"integrity": "sha512-ecpP1HrlP+eb5mNQuz7ObzMWtGJA78UrPlzGRes1KiKJ/c8e1UrrAWI/wuI0Ry7fIKYA6dUzxJ4fHR5TEnMAVA==",
"optional": true
},
"apollo-server-core": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-1.2.0.tgz",
"integrity": "sha1-6FHEdESZG2+J+IUpI3B2uD4B6O4=",
"requires": {
"apollo-cache-control": "0.0.7",
"apollo-tracing": "0.1.1",
"graphql-extensions": "0.0.5"
}
},
"apollo-server-express": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-1.2.0.tgz",
"integrity": "sha1-AmsStFO47KxgRLIFtqhf5Zb7X54=",
"requires": {
"apollo-server-core": "1.2.0",
"apollo-server-module-graphiql": "1.2.0"
}
},
"apollo-server-module-graphiql": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/apollo-server-module-graphiql/-/apollo-server-module-graphiql-1.2.0.tgz",
"integrity": "sha1-iZ2E87dHeV27/INUqlFiLvA4FRw="
},
"apollo-tracing": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/apollo-tracing/-/apollo-tracing-0.1.1.tgz",
"integrity": "sha512-OrL0SYpmwNs6R339y7Is6PppOkyooMB1iLSN+HAp1FdBycQ88SqVV5Dqjxb4Du+TrMyyJLHfR5BAENZSFQyWGQ==",
"requires": {
"graphql-extensions": "0.0.5"
}
},
"asn1": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz",
"integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y="
},
"assert-plus": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
},
"async": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
"integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E="
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
"aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
"integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
},
"aws4": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz",
"integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4="
},
"bcrypt-pbkdf": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz",
"integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=",
"optional": true,
"requires": {
"tweetnacl": "0.14.5"
}
},
"boom": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz",
"integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=",
"requires": {
"hoek": "4.2.0"
}
},
"bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
"integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
},
"caseless": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
},
"co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
"integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ="
},
"combined-stream": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz",
"integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=",
"requires": {
"delayed-stream": "1.0.0"
}
},
"compressible": {
"version": "2.0.12",
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.12.tgz",
"integrity": "sha1-xZpcmdt2dn6YdlAOJx72OzSTvWY=",
"requires": {
"mime-db": "1.30.0"
}
},
"compression": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.7.1.tgz",
"integrity": "sha1-7/JgPvwuIs+G810uuTWJ+YdTc9s=",
"requires": {
"accepts": "1.3.4",
"bytes": "3.0.0",
"compressible": "2.0.12",
"debug": "2.6.9",
"on-headers": "1.0.1",
"safe-buffer": "5.1.1",
"vary": "1.1.2"
}
},
"core-js": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.1.tgz",
"integrity": "sha1-rmh03GaTd4m4B1T/VCjfZoGcpQs="
},
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"cryptiles": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz",
"integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=",
"requires": {
"boom": "5.2.0"
},
"dependencies": {
"boom": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz",
"integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==",
"requires": {
"hoek": "4.2.0"
}
}
}
},
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
"integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
"requires": {
"assert-plus": "1.0.0"
}
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
},
"ecc-jsbn": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz",
"integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=",
"optional": true,
"requires": {
"jsbn": "0.1.1"
}
},
"extend": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
"integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ="
},
"extsprintf": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
},
"fast-deep-equal": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz",
"integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8="
},
"fast-json-stable-stringify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
"integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
},
"flat": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/flat/-/flat-4.0.0.tgz",
@ -12,10 +272,297 @@
"is-buffer": "1.1.5"
}
},
"forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
"integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
},
"form-data": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz",
"integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=",
"requires": {
"asynckit": "0.4.0",
"combined-stream": "1.0.5",
"mime-types": "2.1.17"
}
},
"getpass": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
"integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
"requires": {
"assert-plus": "1.0.0"
}
},
"graphql-extensions": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/graphql-extensions/-/graphql-extensions-0.0.5.tgz",
"integrity": "sha512-IbgYhKIyI60Nio/uJjkkiXaOZ2fI8ynAyzcA/okD0iuKzBdWX4Tn6tidMLgd16Bf2v3TtNnyXnN0F2BJDs6e4A==",
"requires": {
"core-js": "2.5.1",
"source-map-support": "0.5.0"
}
},
"har-schema": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
"integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI="
},
"har-validator": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz",
"integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=",
"requires": {
"ajv": "5.4.0",
"har-schema": "2.0.0"
}
},
"hawk": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz",
"integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==",
"requires": {
"boom": "4.3.1",
"cryptiles": "3.1.2",
"hoek": "4.2.0",
"sntp": "2.1.0"
}
},
"hoek": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz",
"integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ=="
},
"http-signature": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
"integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
"requires": {
"assert-plus": "1.0.0",
"jsprim": "1.4.1",
"sshpk": "1.13.1"
}
},
"is-buffer": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz",
"integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw="
},
"is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
},
"isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
},
"jsbn": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
"optional": true
},
"json-schema": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
"integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
},
"json-schema-traverse": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
"integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A="
},
"json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
},
"jsprim": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
"integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
"requires": {
"assert-plus": "1.0.0",
"extsprintf": "1.3.0",
"json-schema": "0.2.3",
"verror": "1.10.0"
}
},
"mime-db": {
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz",
"integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE="
},
"mime-types": {
"version": "2.1.17",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz",
"integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=",
"requires": {
"mime-db": "1.30.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"negotiator": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
},
"oauth-sign": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz",
"integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM="
},
"on-headers": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz",
"integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c="
},
"performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
},
"punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
},
"qs": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz",
"integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A=="
},
"request": {
"version": "2.83.0",
"resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz",
"integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==",
"requires": {
"aws-sign2": "0.7.0",
"aws4": "1.6.0",
"caseless": "0.12.0",
"combined-stream": "1.0.5",
"extend": "3.0.1",
"forever-agent": "0.6.1",
"form-data": "2.3.1",
"har-validator": "5.0.3",
"hawk": "6.0.2",
"http-signature": "1.2.0",
"is-typedarray": "1.0.0",
"isstream": "0.1.2",
"json-stringify-safe": "5.0.1",
"mime-types": "2.1.17",
"oauth-sign": "0.8.2",
"performance-now": "2.1.0",
"qs": "6.5.1",
"safe-buffer": "5.1.1",
"stringstream": "0.0.5",
"tough-cookie": "2.3.3",
"tunnel-agent": "0.6.0",
"uuid": "3.1.0"
}
},
"safe-buffer": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
},
"sntp": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz",
"integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==",
"requires": {
"hoek": "4.2.0"
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
},
"source-map-support": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.0.tgz",
"integrity": "sha512-vUoN3I7fHQe0R/SJLKRdKYuEdRGogsviXFkHHo17AWaTGv17VLnxw+CFXvqy+y4ORZ3doWLQcxRYfwKrsd/H7Q==",
"requires": {
"source-map": "0.6.1"
}
},
"sshpk": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz",
"integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=",
"requires": {
"asn1": "0.2.3",
"assert-plus": "1.0.0",
"bcrypt-pbkdf": "1.0.1",
"dashdash": "1.14.1",
"ecc-jsbn": "0.1.1",
"getpass": "0.1.7",
"jsbn": "0.1.1",
"tweetnacl": "0.14.5"
}
},
"stream-line-wrapper": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/stream-line-wrapper/-/stream-line-wrapper-0.1.1.tgz",
"integrity": "sha1-Pivh02jGNW+Qru9keGaD8+7j7qc=",
"requires": {
"async": "0.2.10"
}
},
"stringstream": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz",
"integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg="
},
"tough-cookie": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz",
"integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=",
"requires": {
"punycode": "1.4.1"
}
},
"tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
"requires": {
"safe-buffer": "5.1.1"
}
},
"tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
"optional": true
},
"uuid": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz",
"integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g=="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"verror": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
"requires": {
"assert-plus": "1.0.0",
"core-util-is": "1.0.2",
"extsprintf": "1.3.0"
}
}
}
}

View file

@ -1,22 +1,26 @@
{
"name": "Vulcan",
"version": "1.8.0",
"version": "1.8.1",
"engines": {
"npm": "^3.0"
},
"scripts": {
"prestart": "sh prestart_vulcan.sh",
"prestart": "node prestart_vulcan.js",
"start": "meteor --settings settings.json",
"lint": "eslint --cache --ext .jsx,js packages"
},
"dependencies": {
"analytics-node": "^2.1.1",
"apollo-client": "^1.2.2",
"apollo-engine": "^0.5.4",
"apollo-errors": "^1.4.0",
"apollo-server-express": "^1.2.0",
"babel-runtime": "^6.18.0",
"bcrypt": "^0.8.7",
"body-parser": "^1.15.2",
"body-parser": "^1.18.2",
"chalk": "2.2.0",
"classnames": "^2.2.3",
"compression": "^1.7.1",
"cookie-parser": "^1.4.3",
"cross-fetch": "^0.0.8",
"crypto-js": "^3.1.9-1",
@ -30,7 +34,6 @@
"graphql": "^0.9.6",
"graphql-anywhere": "^3.0.1",
"graphql-date": "^1.0.2",
"graphql-server-express": "^0.6.0",
"graphql-tag": "^2.0.0",
"graphql-tools": "^0.10.1",
"graphql-type-json": "^0.1.4",
@ -54,7 +57,7 @@
"react": "^15.6.1",
"react-addons-pure-render-mixin": "^15.4.1",
"react-apollo": "^1.1.1",
"react-bootstrap": "^0.30.7",
"react-bootstrap": "^0.31.3",
"react-bootstrap-datetimepicker": "0.0.22",
"react-cookie": "^0.4.6",
"react-datetime": "^2.3.2",

View file

@ -1,7 +1,7 @@
Package.describe({
name: "boilerplate-generator",
summary: "Generates the boilerplate html from program's manifest",
version: '1.2.0'
version: '1.3.0'
});
Package.onUse(api => {
@ -11,4 +11,4 @@ Package.onUse(api => {
], 'server');
api.mainModule('generator.js', 'server');
api.export('Boilerplate', 'server');
});
});

View file

@ -25,6 +25,12 @@ registerFragment(`
...UsersMinimumInfo
}
}
# vulcan:voting
# voting
currentUserVotes{
...VoteFragment
}
baseScore
score
}
`);

View file

@ -198,10 +198,15 @@ const schema = {
insertableBy: ['admins'],
editableBy: ['admins'],
control: 'select',
onInsert: document => {
if (document.userId && !document.status) {
const user = Users.findOne(document.userId);
return Posts.getDefaultStatus(user);
onInsert: (document, currentUser) => {
if (!document.status) {
return Posts.getDefaultStatus(currentUser);
}
},
onEdit: (modifier, document, currentUser) => {
// if for some reason post status has been removed, give it default status
if (modifier.$unset && modifier.$unset.status) {
return Posts.getDefaultStatus(currentUser);
}
},
form: {
@ -367,8 +372,8 @@ const schema = {
optional: true,
resolveAs: {
type: 'String',
resolver: (booking, args, context) => {
return moment(booking.endAt).format('dddd, MMMM Do YYYY');
resolver: (post, args, context) => {
return moment(post.postedAt).format('dddd, MMMM Do YYYY');
}
}
},
@ -403,6 +408,39 @@ const schema = {
}
},
emailShareUrl: {
type: String,
optional: true,
resolveAs: {
type: 'String',
resolver: (post) => {
return Posts.getEmailShareUrl(post);
}
}
},
twitterShareUrl: {
type: String,
optional: true,
resolveAs: {
type: 'String',
resolver: (post) => {
return Posts.getTwitterShareUrl(post);
}
}
},
facebookShareUrl: {
type: String,
optional: true,
resolveAs: {
type: 'String',
resolver: (post) => {
return Posts.getFacebookShareUrl(post);
}
}
},
};
export default schema;

View file

@ -9,7 +9,8 @@ import { performVoteServer } from 'meteor/vulcan:voting';
*/
function CommentsNewUpvoteOwnComment(comment) {
var commentAuthor = Users.findOne(comment.userId);
return {...comment, ...performVoteServer({ document: comment, voteType: 'upvote', collection: Comments, user: commentAuthor })};
const votedComent = performVoteServer({ document: comment, voteType: 'upvote', collection: Comments, user: commentAuthor })
return {...comment, ...votedComent};
}
addCallback('comments.new.async', CommentsNewUpvoteOwnComment);
addCallback('comments.new.after', CommentsNewUpvoteOwnComment);

View file

@ -96,7 +96,7 @@ Posts.increaseClicks = (post, ip) => {
const existingClickEvent = Events.findOne({name: 'click', 'properties.postId': post._id, 'properties.ip': ip});
if(!existingClickEvent) {
Events.log(clickEvent);
// Events.log(clickEvent); // Sidebar only: don't log event
return Posts.update(post._id, { $inc: { clickCount: 1 }});
}
} else {
@ -112,4 +112,14 @@ function PostsClickTracking(post, ip) {
// note: this event is not sent to segment cause we cannot access the current user
// in our server-side route /out -> sending an event would create a new anonymous
// user: the free limit of 1,000 unique users per month would be reached quickly
addCallback('posts.click.async', PostsClickTracking);
addCallback('posts.click.async', PostsClickTracking);
//////////////////////////////////////////////////////
// posts.approve.sync //
//////////////////////////////////////////////////////
function PostsApprovedSetPostedAt (modifier, post) {
modifier.postedAt = new Date();
return modifier;
}
addCallback('posts.approve.sync', PostsApprovedSetPostedAt);

View file

@ -17,4 +17,4 @@ function PostsNewUpvoteOwnPost(post) {
return {...post, ...performVoteServer({ document: post, voteType: 'upvote', collection: Posts, user: postAuthor })};
}
addCallback('posts.new.async', PostsNewUpvoteOwnPost);
addCallback('posts.new.after', PostsNewUpvoteOwnPost);

View file

@ -1,7 +1,7 @@
Package.describe({
name: "example-forum",
summary: "Vulcan forum package",
version: '1.8.0',
version: '1.8.1',
git: "https://github.com/VulcanJS/Vulcan.git"
});
@ -14,11 +14,11 @@ Package.onUse(function (api) {
'fourseven:scss@4.5.0',
// vulcan core
'vulcan:core@1.8.0',
'vulcan:core@1.8.1',
// vulcan packages
'vulcan:voting@1.8.0',
'vulcan:accounts@1.8.0',
'vulcan:voting@1.8.1',
'vulcan:accounts@1.8.1',
'vulcan:email',
'vulcan:forms',
'vulcan:newsletter',

View file

@ -11,7 +11,7 @@ component (if the "component" prop is specified).
import React from 'react';
import { registerComponent, Components, withCurrentUser } from 'meteor/vulcan:core';
import Users from 'meteor/vulcan:users';
import PicsNewForm from '../pics/PicsNewForm';
// import PicsNewForm from '../pics/PicsNewForm';
// navigation bar component when the user is logged in
@ -33,7 +33,7 @@ const NavLoggedIn = ({currentUser}) =>
</div>
<Components.ModalTrigger label="Upload">
<PicsNewForm />
<Components.PicsNewForm />
</Components.ModalTrigger>
</div>
@ -71,4 +71,4 @@ const Header = ({currentUser}) =>
</div>
registerComponent('Header', Header, withCurrentUser);
registerComponent('Header', Header, withCurrentUser);

View file

@ -11,7 +11,7 @@ component (if the "component" prop is specified).
import React from 'react';
import { registerComponent, Components, withCurrentUser } from 'meteor/vulcan:core';
import Users from 'meteor/vulcan:users';
import PicsNewForm from '../pics/PicsNewForm';
// import PicsNewForm from '../pics/PicsNewForm';
// navigation bar component when the user is logged in
@ -33,7 +33,7 @@ const NavLoggedIn = ({currentUser}) =>
</div>
<Components.ModalTrigger label="Upload">
<PicsNewForm />
<Components.PicsNewForm />
</Components.ModalTrigger>
</div>
@ -71,4 +71,4 @@ const Header = ({currentUser}) =>
</div>
registerComponent('Header', Header, withCurrentUser);
registerComponent('Header', Header, withCurrentUser);

View file

@ -11,7 +11,7 @@ component (if the "component" prop is specified).
import React from 'react';
import { registerComponent, Components, withCurrentUser } from 'meteor/vulcan:core';
import Users from 'meteor/vulcan:users';
import PicsNewForm from '../pics/PicsNewForm';
// import PicsNewForm from '../pics/PicsNewForm';
// navigation bar component when the user is logged in
@ -34,7 +34,7 @@ const NavLoggedIn = ({currentUser}) =>
{Users.canDo(currentUser, 'pics.new') ?
<Components.ModalTrigger label="Upload">
<PicsNewForm />
<Components.PicsNewForm />
</Components.ModalTrigger>
: null
}

View file

@ -21,14 +21,14 @@ Accounts.ui._options = {
passwordSignupFields: 'USERNAME_AND_EMAIL',
minimumPasswordLength: 7,
loginPath: '/',
signUpPath: null,
signUpPath: '/',
resetPasswordPath: null,
profilePath: '/',
changePasswordPath: null,
homeRoutePath: '/',
onSubmitHook: () => {},
onPreSignUpHook: () => new Promise(resolve => resolve()),
onPostSignUpHook: () => {},
onPostSignUpHook: () => redirect(`${Accounts.ui._options.signUpPath}`),
onEnrollAccountHook: () => redirect(`${Accounts.ui._options.loginPath}`),
onResetPasswordHook: () => redirect(`${Accounts.ui._options.loginPath}`),
onVerifyEmailHook: () => redirect(`${Accounts.ui._options.profilePath}`),

View file

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

View file

@ -1,7 +1,7 @@
Package.describe({
name: "vulcan:admin",
summary: "Vulcan components package",
version: '1.8.0',
version: '1.8.1',
git: "https://github.com/VulcanJS/Vulcan.git"
});
@ -14,7 +14,7 @@ Package.onUse(function (api) {
'fourseven:scss@4.5.0',
'dynamic-import@0.1.1',
// Vulcan packages
'vulcan:core@1.8.0',
'vulcan:core@1.8.1',
]);

View file

@ -1,7 +1,7 @@
Package.describe({
name: 'vulcan:cloudinary',
summary: 'Vulcan file upload package.',
version: '1.8.0',
version: '1.8.1',
git: "https://github.com/VulcanJS/Vulcan.git"
});
@ -10,7 +10,7 @@ Package.onUse(function (api) {
api.versionsFrom('METEOR@1.5.2');
api.use([
'vulcan:core@1.8.0'
'vulcan:core@1.8.1'
]);
api.mainModule("lib/client/main.js", "client");

View file

@ -12,7 +12,7 @@ import { addCallback, getActions } from 'meteor/vulcan:lib';
* @param {Object} Redux store reference instantiated on the current connected client
* @param {Object} Apollo Client reference instantiated on the current connected client
*/
function RouterClearMessages(unusedItem, store, apolloClient) {
function RouterClearMessages(unusedItem, nextRoute, store, apolloClient) {
store.dispatch(getActions().messages.clearSeen());
return unusedItem;

View file

@ -1,36 +1,68 @@
import { Components, registerComponent, registerSetting, getSetting, Strings } from 'meteor/vulcan:lib';
import {
Components,
registerComponent,
registerSetting,
getSetting,
Strings,
runCallbacks,
} from 'meteor/vulcan:lib';
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { IntlProvider, intlShape} from 'meteor/vulcan:i18n';
import { IntlProvider, intlShape } from 'meteor/vulcan:i18n';
import withCurrentUser from '../containers/withCurrentUser.js';
class App extends PureComponent {
constructor(props) {
super(props);
if (props.currentUser) {
runCallbacks('events.identify', props.currentUser);
}
}
getLocale() {
return getSetting('locale', 'en');
}
getChildContext() {
const messages = Strings[this.getLocale()] || {};
const intlProvider = new IntlProvider({locale: this.getLocale()}, messages);
const intlProvider = new IntlProvider(
{ locale: this.getLocale() },
messages
);
const { intl } = intlProvider.getChildContext();
return {
intl: intl
intl: intl,
};
}
componentWillUpdate(nextProps) {
if (!this.props.currentUser && nextProps.currentUser) {
runCallbacks('events.identify', nextProps.currentUser);
}
}
render() {
const currentRoute = _.last(this.props.routes);
const LayoutComponent = currentRoute.layoutName ? Components[currentRoute.layoutName] : Components.Layout;
const LayoutComponent = currentRoute.layoutName
? Components[currentRoute.layoutName]
: Components.Layout;
return (
<IntlProvider locale={this.getLocale()} messages={Strings[this.getLocale()]}>
<IntlProvider
locale={this.getLocale()}
messages={Strings[this.getLocale()]}
>
<div>
<Components.HeadTags />
<Components.RouterHook currentRoute={currentRoute} />
<LayoutComponent {...this.props} currentRoute={currentRoute}>
{ this.props.currentUserLoading ? <Components.Loading /> : (this.props.children ? this.props.children : <Components.Welcome />) }
{this.props.currentUserLoading ? (
<Components.Loading />
) : this.props.children ? (
this.props.children
) : (
<Components.Welcome />
)}
</LayoutComponent>
</div>
</IntlProvider>
@ -40,11 +72,11 @@ class App extends PureComponent {
App.propTypes = {
currentUserLoading: PropTypes.bool,
}
};
App.childContextTypes = {
intl: intlShape,
}
};
App.displayName = 'App';

View file

@ -112,10 +112,12 @@ DatatableContents Component
*/
const DatatableContents = (props) => {
const {collection, columns, results, loading, loadMore, count, totalCount, networkStatus, showEdit, currentUser} = props;
const {collection, columns, results, loading, loadMore, count, totalCount, networkStatus, showEdit, currentUser, emptyState} = props;
if (loading) {
return <Components.Loading />;
} else if (!results.length) {
return emptyState || null;
}
const isLoadingMore = networkStatus === 2;
@ -123,26 +125,26 @@ const DatatableContents = (props) => {
return (
<div className="datatable-list">
<table className="table">
<thead>
<tr>
{_.sortBy(columns, column => column.order).map((column, index) => <Components.DatatableHeader key={index} collection={collection} column={column}/>)}
{showEdit ? <th><FormattedMessage id="datatable.edit"/></th> : null}
</tr>
</thead>
<tbody>
{results.map((document, index) => <Components.DatatableRow collection={collection} columns={columns} document={document} key={index} showEdit={showEdit} currentUser={currentUser}/>)}
</tbody>
</table>
<div className="admin-users-load-more">
{hasMore ?
isLoadingMore ?
<Components.Loading/>
: <Button bsStyle="primary" onClick={e => {e.preventDefault(); loadMore();}}>Load More ({count}/{totalCount})</Button>
: null
}
<table className="table">
<thead>
<tr>
{_.sortBy(columns, column => column.order).map((column, index) => <Components.DatatableHeader key={index} collection={collection} column={column}/>)}
{showEdit ? <th><FormattedMessage id="datatable.edit"/></th> : null}
</tr>
</thead>
<tbody>
{results.map((document, index) => <Components.DatatableRow collection={collection} columns={columns} document={document} key={index} showEdit={showEdit} currentUser={currentUser}/>)}
</tbody>
</table>
<div className="admin-users-load-more">
{hasMore ?
isLoadingMore ?
<Components.Loading/>
: <Button bsStyle="primary" onClick={e => {e.preventDefault(); loadMore();}}>Load More ({count}/{totalCount})</Button>
: null
}
</div>
</div>
</div>
)
}
registerComponent('DatatableContents', DatatableContents);

View file

@ -0,0 +1,20 @@
import { Components, registerComponent } from 'meteor/vulcan:lib';
import React from 'react';
import Button from 'react-bootstrap/lib/Button';
import { FormattedMessage, intlShape } from 'meteor/vulcan:i18n';
const EditButton = ({ collection, document, bsStyle = 'primary' }, {intl}) =>
<Components.ModalTrigger
label={intl.formatMessage({id: 'datatable.edit'})}
component={<Button bsStyle={bsStyle}><FormattedMessage id="datatable.edit" /></Button>}
>
<Components.DatatableEditForm collection={collection} document={document} />
</Components.ModalTrigger>
EditButton.contextTypes = {
intl: intlShape
};
EditButton.displayName = 'EditButton';
registerComponent('EditButton', EditButton);

View file

@ -2,6 +2,7 @@ import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import Helmet from 'react-helmet';
import { registerComponent, Utils, getSetting, registerSetting, Head } from 'meteor/vulcan:lib';
import { compose } from 'react-apollo';
registerSetting('logoUrl', null, 'Absolute URL for the logo image');
registerSetting('title', 'My App', 'App title');
@ -13,9 +14,9 @@ registerSetting('faviconUrl', '/img/favicon.ico', 'Favicon absolute URL');
class HeadTags extends PureComponent {
render() {
const url = !!this.props.url ? this.props.url : Utils.getSiteUrl();
const title = !!this.props.title ? this.props.title : getSetting('title', 'My App');
const description = !!this.props.description ? this.props.description : getSetting('tagline') || getSetting('description');
const url = this.props.url || Utils.getSiteUrl();
const title = this.props.title || getSetting('title', 'My App');
const description = this.props.description || getSetting('tagline') || getSetting('description');
// default image meta: logo url, else site image defined in settings
let image = !!getSetting('siteImage') ? getSetting('siteImage'): getSetting('logoUrl');
@ -27,6 +28,10 @@ class HeadTags extends PureComponent {
// add site url base if the image is stored locally
if (!!image && image.indexOf('//') === -1) {
// remove starting slash from image path if needed
if (image.charAt(0) === '/') {
image = image.slice(1);
}
image = Utils.getSiteUrl() + image;
}
@ -58,9 +63,21 @@ class HeadTags extends PureComponent {
{Head.meta.map((tag, index) => <meta key={index} {...tag}/>)}
{Head.link.map((tag, index) => <link key={index} {...tag}/>)}
{Head.script.map((tag, index) => <script key={index} {...tag}/>)}
{Head.script.map((tag, index) => <script key={index} {...tag}>{contents}</script>)}
</Helmet>
{Head.components.map((componentOrArray, index) => {
let HeadComponent;
if (Array.isArray(componentOrArray)) {
const [component, ...hocs] = componentOrArray;
HeadComponent = compose(...hocs)(component);
} else {
HeadComponent = componentOrArray;
}
return <HeadComponent key={index} />
})}
</div>
);
}

View file

@ -0,0 +1,26 @@
import React, { PureComponent } from 'react';
import { registerComponent, runCallbacks } from 'meteor/vulcan:lib';
import { withApollo } from 'react-apollo';
class RouterHook extends PureComponent {
constructor(props) {
super(props);
this.runOnUpdateCallback(props);
}
componentWillReceiveProps(nextProps) {
this.runOnUpdateCallback(nextProps);
}
runOnUpdateCallback = props => {
const { currentRoute, client } = props;
// the first argument is an item to iterate on, needed by vulcan:lib/callbacks
// note: this item is not used in this specific callback: router.onUpdate
runCallbacks('router.onUpdate', {}, currentRoute, client.store, client);
};
render() {
return null;
}
}
registerComponent('RouterHook', RouterHook, withApollo);

View file

@ -1,11 +1,11 @@
import React, { PropTypes, Component } from 'react';
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
import { getFragment, getFragmentName } from 'meteor/vulcan:core';
import { getSetting, getFragment, getFragmentName } from 'meteor/vulcan:core';
export default function withDocument (options) {
const { collection, pollInterval = 20000 } = options,
const { collection, pollInterval = getSetting('pollInterval', 20000), enableCache = false } = options,
queryName = options.queryName || `${collection.options.collectionName}SingleQuery`,
singleResolverName = collection.options.resolvers.single && collection.options.resolvers.single.name;
@ -22,8 +22,8 @@ export default function withDocument (options) {
const fragmentName = getFragmentName(fragment);
return graphql(gql`
query ${queryName}($documentId: String, $slug: String) {
${singleResolverName}(documentId: $documentId, slug: $slug) {
query ${queryName}($documentId: String, $slug: String, $enableCache: Boolean) {
${singleResolverName}(documentId: $documentId, slug: $slug, enableCache: $enableCache) {
__typename
...${fragmentName}
}
@ -33,17 +33,24 @@ export default function withDocument (options) {
alias: 'withDocument',
options(ownProps) {
return {
variables: { documentId: ownProps.documentId, slug: ownProps.slug },
const graphQLOptions = {
variables: { documentId: ownProps.documentId, slug: ownProps.slug, enableCache },
pollInterval, // note: pollInterval can be set to 0 to disable polling (20s by default)
};
if (options.fetchPolicy) {
graphQLOptions.fetchPolicy = options.fetchPolicy;
}
return graphQLOptions;
},
props: returnedProps => {
const { ownProps, data } = returnedProps;
const propertyName = options.propertyName || 'document';
return {
loading: data.loading,
// document: Utils.convertDates(collection, data[singleResolverName]),
document: data[singleResolverName],
[ propertyName ]: data[singleResolverName],
fragmentName,
fragment,
};

View file

@ -38,7 +38,7 @@ import React, { PropTypes, Component } from 'react';
import { withApollo, graphql } from 'react-apollo';
import gql from 'graphql-tag';
import update from 'immutability-helper';
import { getFragment, getFragmentName } from 'meteor/vulcan:core';
import { getSetting, getFragment, getFragmentName } from 'meteor/vulcan:core';
import Mingo from 'mingo';
import compose from 'recompose/compose';
import withState from 'recompose/withState';
@ -47,7 +47,7 @@ const withList = (options) => {
// console.log(options)
const { collection, limit = 10, pollInterval = 20000, totalResolver = true } = options,
const { collection, limit = 10, pollInterval = getSetting('pollInterval', 20000), totalResolver = true, enableCache = false } = options,
queryName = options.queryName || `${collection.options.collectionName}ListQuery`,
listResolverName = collection.options.resolvers.list && collection.options.resolvers.list.name,
totalResolverName = collection.options.resolvers.total && collection.options.resolvers.total.name;
@ -66,9 +66,9 @@ const withList = (options) => {
// build graphql query from options
const query = gql`
query ${queryName}($terms: JSON) {
${totalResolver ? `${totalResolverName}(terms: $terms)` : ``}
${listResolverName}(terms: $terms) {
query ${queryName}($terms: JSON, $enableCache: Boolean) {
${totalResolver ? `${totalResolverName}(terms: $terms, enableCache: $enableCache)` : ``}
${listResolverName}(terms: $terms, enableCache: $enableCache) {
__typename
...${fragmentName}
}
@ -106,9 +106,11 @@ const withList = (options) => {
options({terms, paginationTerms, client: apolloClient}) {
// get terms from options, then props, then pagination
const mergedTerms = {...options.terms, ...terms, ...paginationTerms};
return {
const graphQLOptions = {
variables: {
terms: mergedTerms,
enableCache,
},
// note: pollInterval can be set to 0 to disable polling (20s by default)
pollInterval,
@ -119,18 +121,27 @@ const withList = (options) => {
},
};
if (options.fetchPolicy) {
graphQLOptions.fetchPolicy = options.fetchPolicy
}
return graphQLOptions;
},
// define props returned by graphql HoC
props(props) {
// see https://github.com/apollographql/apollo-client/blob/master/packages/apollo-client/src/core/networkStatus.ts
const refetch = props.data.refetch,
// results = Utils.convertDates(collection, props.data[listResolverName]),
results = props.data[listResolverName],
totalCount = props.data[totalResolverName],
networkStatus = props.data.networkStatus,
loading = props.data.loading,
error = props.data.error;
loading = props.data.networkStatus === 1,
loadingMore = props.data.networkStatus === 2,
error = props.data.error,
propertyName = options.propertyName || 'results';
if (error) {
console.log(error);
@ -139,8 +150,9 @@ const withList = (options) => {
return {
// see https://github.com/apollostack/apollo-client/blob/master/src/queries/store.ts#L28-L36
// note: loading will propably change soon https://github.com/apollostack/apollo-client/issues/831
loading: networkStatus === 1,
results,
loading,
loadingMore,
[ propertyName ]: results,
totalCount,
refetch,
networkStatus,

View file

@ -4,121 +4,213 @@ Default mutations
*/
import { newMutation, editMutation, removeMutation, Utils } from 'meteor/vulcan:lib';
import { registerCallback, newMutation, editMutation, removeMutation, Utils } from 'meteor/vulcan:lib';
import Users from 'meteor/vulcan:users';
export const getDefaultMutations = (collectionName, options = {}) => ({
export const getDefaultMutations = (collectionName, options = {}) => {
// mutation for inserting a new document
// register callbacks for documentation purposes
registerCollectionCallbacks(collectionName);
new: {
name: `${collectionName}New`,
// check function called on a user to see if they can perform the operation
check(user, document) {
if (options.newCheck) {
return options.newCheck(user, document);
}
// if user is not logged in, disallow operation
if (!user) return false;
// else, check if they can perform "foo.new" operation (e.g. "movies.new")
return Users.canDo(user, `${collectionName.toLowerCase()}.new`);
},
async mutation(root, {document}, context) {
return {
// mutation for inserting a new document
new: {
const collection = context[collectionName];
// check if current user can pass check function; else throw error
Utils.performCheck(this.check, context.currentUser, document);
// pass document to boilerplate newMutation function
return await newMutation({
collection,
document: document,
currentUser: context.currentUser,
validate: true,
context,
});
},
},
// mutation for editing a specific document
edit: {
name: `${collectionName}Edit`,
// check function called on a user and document to see if they can perform the operation
check(user, document) {
if (options.editCheck) {
return options.editCheck(user, document);
}
if (!user || !document) return false;
// check if user owns the document being edited.
// if they do, check if they can perform "foo.edit.own" action
// if they don't, check if they can perform "foo.edit.all" action
return Users.owns(user, document) ? Users.canDo(user, `${collectionName.toLowerCase()}.edit.own`) : Users.canDo(user, `${collectionName.toLowerCase()}.edit.all`);
},
async mutation(root, {documentId, set, unset}, context) {
const collection = context[collectionName];
// get entire unmodified document from database
const document = collection.findOne(documentId);
// check if user can perform operation; if not throw error
Utils.performCheck(this.check, context.currentUser, document);
// call editMutation boilerplate function
return await editMutation({
collection,
documentId: documentId,
set: set,
unset: unset,
currentUser: context.currentUser,
validate: true,
context,
});
},
},
// mutation for removing a specific document (same checks as edit mutation)
remove: {
name: `${collectionName}Remove`,
check(user, document) {
if (options.removeCheck) {
return options.removeCheck(user, document);
}
name: `${collectionName}New`,
if (!user || !document) return false;
return Users.owns(user, document) ? Users.canDo(user, `${collectionName.toLowerCase()}.remove.own`) : Users.canDo(user, `${collectionName.toLowerCase()}.remove.all`);
// check function called on a user to see if they can perform the operation
check(user, document) {
if (options.newCheck) {
return options.newCheck(user, document);
}
// if user is not logged in, disallow operation
if (!user) return false;
// else, check if they can perform "foo.new" operation (e.g. "movies.new")
return Users.canDo(user, `${collectionName.toLowerCase()}.new`);
},
async mutation(root, {document}, context) {
const collection = context[collectionName];
// check if current user can pass check function; else throw error
Utils.performCheck(this.check, context.currentUser, document);
// pass document to boilerplate newMutation function
return await newMutation({
collection,
document: document,
currentUser: context.currentUser,
validate: true,
context,
});
},
},
// mutation for editing a specific document
edit: {
name: `${collectionName}Edit`,
// check function called on a user and document to see if they can perform the operation
check(user, document) {
if (options.editCheck) {
return options.editCheck(user, document);
}
if (!user || !document) return false;
// check if user owns the document being edited.
// if they do, check if they can perform "foo.edit.own" action
// if they don't, check if they can perform "foo.edit.all" action
return Users.owns(user, document) ? Users.canDo(user, `${collectionName.toLowerCase()}.edit.own`) : Users.canDo(user, `${collectionName.toLowerCase()}.edit.all`);
},
async mutation(root, {documentId, set, unset}, context) {
const collection = context[collectionName];
// get entire unmodified document from database
const document = collection.findOne(documentId);
// check if user can perform operation; if not throw error
Utils.performCheck(this.check, context.currentUser, document);
// call editMutation boilerplate function
return await editMutation({
collection,
documentId: documentId,
set: set,
unset: unset,
currentUser: context.currentUser,
validate: true,
context,
});
},
},
async mutation(root, {documentId}, context) {
// mutation for removing a specific document (same checks as edit mutation)
const collection = context[collectionName];
remove: {
const document = collection.findOne(documentId);
Utils.performCheck(this.check, context.currentUser, document, context);
name: `${collectionName}Remove`,
check(user, document) {
if (options.removeCheck) {
return options.removeCheck(user, document);
}
if (!user || !document) return false;
return Users.owns(user, document) ? Users.canDo(user, `${collectionName.toLowerCase()}.remove.own`) : Users.canDo(user, `${collectionName.toLowerCase()}.remove.all`);
},
async mutation(root, {documentId}, context) {
const collection = context[collectionName];
const document = collection.findOne(documentId);
Utils.performCheck(this.check, context.currentUser, document, context);
return await removeMutation({
collection,
documentId: documentId,
currentUser: context.currentUser,
validate: true,
context,
});
},
return await removeMutation({
collection,
documentId: documentId,
currentUser: context.currentUser,
validate: true,
context,
});
},
}
},
};
});
const registerCollectionCallbacks = collectionName => {
collectionName = collectionName.toLowerCase();
registerCallback({
name: `${collectionName}.new.validate`,
arguments: [{document: 'The document being inserted'}, {currentUser: 'The current user'}, {validationErrors: 'An object that can be used to accumulate validation errors'}],
runs: 'sync',
returns: 'document',
description: `Validate a document before insertion (can be skipped when inserting directly on server).`
});
registerCallback({
name: `${collectionName}.new.before`,
arguments: [{document: 'The document being inserted'}, {currentUser: 'The current user'}],
runs: 'sync',
returns: 'document',
description: `Perform operations on a new document before it's inserted in the database.`
});
registerCallback({
name: `${collectionName}.new.after`,
arguments: [{document: 'The document being inserted'}, {currentUser: 'The current user'}],
runs: 'sync',
returns: 'document',
description: `Perform operations on a new document after it's inserted in the database but *before* the mutation returns it.`
});
registerCallback({
name: `${collectionName}.new.async`,
arguments: [{document: 'The document being inserted'}, {currentUser: 'The current user'}, {collection: 'The collection the document belongs to'}],
runs: 'async',
returns: null,
description: `Perform operations on a new document after it's inserted in the database asynchronously.`
});
registerCallback({
name: `${collectionName}.edit.validate`,
arguments: [{modifier: 'The MongoDB modifier'}, {document: 'The document being edited'}, {currentUser: 'The current user'}, {validationErrors: 'An object that can be used to accumulate validation errors'}],
runs: 'sync',
returns: 'modifier',
description: `Validate a document before update (can be skipped when updating directly on server).`
});
registerCallback({
name: `${collectionName}.edit.before`,
arguments: [{modifier: 'The MongoDB modifier'}, {document: 'The document being edited'}, {currentUser: 'The current user'}],
runs: 'sync',
returns: 'modifier',
description: `Perform operations on a document before it's updated in the database.`
});
registerCallback({
name: `${collectionName}.edit.after`,
arguments: [{modifier: 'The MongoDB modifier'}, {document: 'The document being edited'}, {currentUser: 'The current user'}],
runs: 'sync',
returns: 'document',
description: `Perform operations on a document after it's updated in the database but *before* the mutation returns it.`
});
registerCallback({
name: `${collectionName}.edit.async`,
arguments: [{newDocument: 'The document after the edit'}, {document: 'The document before the edit'}, {currentUser: 'The current user'}, {collection: 'The collection the document belongs to'}],
runs: 'async',
returns: null,
description: `Perform operations on a document after it's updated in the database asynchronously.`
});
registerCallback({
name: `${collectionName}.remove.validate`,
arguments: [{document: 'The document being removed'}, {currentUser: 'The current user'}, {validationErrors: 'An object that can be used to accumulate validation errors'}],
runs: 'sync',
returns: 'document',
description: `Validate a document before removal (can be skipped when removing directly on server).`
});
registerCallback({
name: `${collectionName}.remove.before`,
arguments: [{document: 'The document being removed'}, {currentUser: 'The current user'}],
runs: 'sync',
returns: null,
description: `Perform operations on a document before it's removed from the database.`
});
registerCallback({
name: `${collectionName}.remove.async`,
arguments: [{document: 'The document being removed'}, {currentUser: 'The current user'}, {collection: 'The collection the document belongs to'}],
runs: 'async',
returns: null,
description: `Perform operations on a document after it's removed from the database asynchronously.`
});
}

View file

@ -6,97 +6,123 @@ Default list, single, and total resolvers
import { Utils, debug } from 'meteor/vulcan:core';
export const getDefaultResolvers = collectionName => ({
const defaultOptions = {
cacheMaxAge: 300
}
// resolver for returning a list of documents based on a set of query terms
export const getDefaultResolvers = (collectionName, resolverOptions = defaultOptions) => {
list: {
return {
name: `${collectionName}List`,
// resolver for returning a list of documents based on a set of query terms
async resolver(root, {terms = {}}, context, info) {
list: {
debug(`//--------------- start ${collectionName} list resolver ---------------//`);
debug(terms);
name: `${collectionName}List`,
// get currentUser and Users collection from context
const { currentUser, Users } = context;
async resolver(root, {terms = {}, enableCache = false}, context, { cacheControl }) {
// get collection based on collectionName argument
const collection = context[collectionName];
debug(`//--------------- start ${collectionName} list resolver ---------------//`);
debug(resolverOptions);
debug(terms);
// get selector and options from terms and perform Mongo query
let {selector, options} = await collection.getParameters(terms, {}, context);
options.skip = terms.offset;
if (cacheControl && enableCache) {
const maxAge = resolverOptions.cacheMaxAge || defaultOptions.cacheMaxAge;
cacheControl.setCacheHint({ maxAge });
}
debug({ selector, options });
// get currentUser and Users collection from context
const { currentUser, Users } = context;
const docs = collection.find(selector, options).fetch();
// get collection based on collectionName argument
const collection = context[collectionName];
// if collection has a checkAccess function defined, remove any documents that doesn't pass the check
const viewableDocs = collection.checkAccess ? _.filter(docs, doc => collection.checkAccess(currentUser, doc)) : docs;
// get selector and options from terms and perform Mongo query
let {selector, options} = await collection.getParameters(terms, {}, context);
options.skip = terms.offset;
debug({ selector, options });
const docs = collection.find(selector, options).fetch();
// if collection has a checkAccess function defined, remove any documents that doesn't pass the check
const viewableDocs = collection.checkAccess ? _.filter(docs, doc => collection.checkAccess(currentUser, doc)) : docs;
// take the remaining documents and remove any fields that shouldn't be accessible
const restrictedDocs = Users.restrictViewableFields(currentUser, collection, viewableDocs);
// prime the cache
restrictedDocs.forEach(doc => collection.loader.prime(doc._id, doc));
debug(`// ${restrictedDocs.length} documents returned`);
debug(`//--------------- end ${collectionName} list resolver ---------------//`);
// return results
return restrictedDocs;
},
},
// resolver for returning a single document queried based on id or slug
single: {
// take the remaining documents and remove any fields that shouldn't be accessible
const restrictedDocs = Users.restrictViewableFields(currentUser, collection, viewableDocs);
name: `${collectionName}Single`,
// prime the cache
restrictedDocs.forEach(doc => collection.loader.prime(doc._id, doc));
async resolver(root, {documentId, slug, enableCache = false}, context, { cacheControl }) {
debug(`// ${restrictedDocs.length} documents returned`);
debug(`//--------------- end ${collectionName} list resolver ---------------//`);
debug(`//--------------- start ${collectionName} single resolver ---------------//`);
debug(resolverOptions);
debug(documentId);
// return results
return restrictedDocs;
if (cacheControl && enableCache) {
const maxAge = resolverOptions.cacheMaxAge || defaultOptions.cacheMaxAge;
cacheControl.setCacheHint({ maxAge });
}
const { currentUser, Users } = context;
const collection = context[collectionName];
// don't use Dataloader if doc is selected by slug
const doc = documentId ? await collection.loader.load(documentId) : (slug ? collection.findOne({slug}) : collection.findOne());
// if collection has a checkAccess function defined, use it to perform a check on the current document
// (will throw an error if check doesn't pass)
if (collection.checkAccess) {
Utils.performCheck(collection.checkAccess, currentUser, doc, collection, documentId);
}
const restrictedDoc = Users.restrictViewableFields(currentUser, collection, doc);
debug(`//--------------- end ${collectionName} single resolver ---------------//`);
// filter out disallowed properties and return resulting document
return restrictedDoc;
},
},
},
// resolver for returning the total number of documents matching a set of query terms
// resolver for returning a single document queried based on id or slug
single: {
name: `${collectionName}Single`,
async resolver(root, {documentId, slug}, context) {
debug(`//--------------- start ${collectionName} single resolver ---------------//`);
debug(documentId);
const { currentUser, Users } = context;
const collection = context[collectionName];
// don't use Dataloader if doc is selected by slug
const doc = documentId ? await collection.loader.load(documentId) : (slug ? collection.findOne({slug}) : collection.findOne());
// if collection has a checkAccess function defined, use it to perform a check on the current document
// (will throw an error if check doesn't pass)
if (collection.checkAccess) {
Utils.performCheck(collection.checkAccess, currentUser, doc, collection, documentId);
}
debug(`//--------------- end ${collectionName} single resolver ---------------//`);
// filter out disallowed properties and return resulting document
return Users.restrictViewableFields(currentUser, collection, doc);
},
},
// resolver for returning the total number of documents matching a set of query terms
total: {
name: `${collectionName}Total`,
async resolver(root, {terms}, context) {
total: {
const collection = context[collectionName];
name: `${collectionName}Total`,
async resolver(root, {terms, enableCache}, context, { cacheControl }) {
if (cacheControl && enableCache) {
const maxAge = resolverOptions.cacheMaxAge || defaultOptions.cacheMaxAge;
cacheControl.setCacheHint({ maxAge });
}
const {selector} = await collection.getParameters(terms, {}, context);
const collection = context[collectionName];
return collection.find(selector).count();
},
const {selector} = await collection.getParameters(terms, {}, context);
return collection.find(selector).count();
},
}
}
});
};

View file

@ -12,6 +12,7 @@ export { default as Icon } from "./components/Icon.jsx";
export { default as Loading } from "./components/Loading.jsx";
export { default as ShowIf } from "./components/ShowIf.jsx";
export { default as ModalTrigger } from './components/ModalTrigger.jsx';
export { default as EditButton } from './components/EditButton.jsx';
export { default as Error404 } from './components/Error404.jsx';
export { default as DynamicLoading } from './components/DynamicLoading.jsx';
export { default as HeadTags } from './components/HeadTags.jsx';
@ -21,6 +22,7 @@ export { default as Datatable } from './components/Datatable.jsx';
export { default as Flash } from './components/Flash.jsx';
export { default as HelloWorld } from './components/HelloWorld.jsx';
export { default as Welcome } from './components/Welcome.jsx';
export { default as RouterHook } from './components/RouterHook.jsx';
export { default as withMessages } from "./containers/withMessages.js";
export { default as withList } from './containers/withList.js';

View file

@ -1,7 +1,7 @@
Package.describe({
name: "vulcan:core",
summary: "Vulcan core package",
version: '1.8.0',
version: '1.8.1',
git: "https://github.com/VulcanJS/Vulcan.git"
});
@ -10,15 +10,15 @@ Package.onUse(function(api) {
api.versionsFrom('METEOR@1.5.2');
api.use([
'vulcan:lib@1.8.0',
'vulcan:i18n@1.8.0',
'vulcan:users@1.8.0',
'vulcan:routing@1.8.0',
'vulcan:debug@1.8.0',
'vulcan:lib@1.8.1',
'vulcan:i18n@1.8.1',
'vulcan:users@1.8.1',
'vulcan:routing@1.8.1',
'vulcan:debug@1.8.1',
]);
api.imply([
'vulcan:lib@1.8.0'
'vulcan:lib@1.8.1'
]);
api.mainModule('lib/server/main.js', 'server');

View file

@ -0,0 +1,31 @@
import React from 'react';
import { FormattedMessage } from 'meteor/vulcan:i18n';
import { registerComponent, Components } from 'meteor/vulcan:lib';
import Callbacks from '../modules/callbacks/collection.js';
const CallbacksName = ({ document }) =>
<strong>{document.name}</strong>
const CallbacksDashboard = props =>
<div className="settings">
<Components.Datatable
showSearch={false}
showEdit={false}
collection={Callbacks}
options={{
fragmentName: 'CallbacksFragment'
}}
columns={[
{ name: 'name', component: CallbacksName },
'arguments',
'returns',
'runs',
'description',
'hooks',
]}
/>
</div>
registerComponent('Callbacks', CallbacksDashboard);
export default Callbacks;

View file

@ -37,7 +37,7 @@ class Email extends PureComponent {
<td>{name}</td>
<td><a href={"/email/template/"+email.template} target="_blank">{email.template}</a></td>
<td>{typeof email.subject === 'function' ? email.subject({}) : email.subject}</td>
<td><a href={email.path.replace(':_id?', '')} target="_blank">{email.path}</a></td>
<td><a href={email.path.replace(':_id?', '').replace(':documentId?', '')} target="_blank">{email.path}</a></td>
<td>
<div className={this.state.loading ? "test-email loading" : "test-email"}>
<Button disabled={this.state.loading} onClick={this.sendTest} bsStyle="primary">Send Test</Button>

View file

@ -0,0 +1,19 @@
import { createCollection } from 'meteor/vulcan:lib';
import schema from './schema.js';
import resolvers from './resolvers.js';
import './fragments.js';
const Callbacks = createCollection({
collectionName: 'Callbacks',
typeName: 'Callback',
schema,
resolvers,
});
export default Callbacks;

View file

@ -0,0 +1,12 @@
import { registerFragment } from 'meteor/vulcan:lib';
registerFragment(`
fragment CallbacksFragment on Callback {
name
arguments
runs
returns
description
hooks
}
`);

View file

@ -0,0 +1,26 @@
import { CallbackHooks } from 'meteor/vulcan:lib';
const resolvers = {
list: {
name: 'CallbacksList',
resolver(root, {terms = {}}, context, info) {
return CallbackHooks;
},
},
total: {
name: 'CallbacksTotal',
resolver(root, {terms = {}}, context) {
return CallbackHooks.length;
},
}
};
export default resolvers;

View file

@ -0,0 +1,57 @@
import { Callbacks } from 'meteor/vulcan:lib';
const schema = {
name: {
label: 'Name',
type: String,
viewableBy: ['admins'],
},
arguments: {
label: 'Arguments',
type: Array,
viewableBy: ['admins'],
},
'arguments.$': {
type: Object,
viewableBy: ['admins'],
},
runs: {
label: 'Runs',
type: String,
viewableBy: ['admins'],
},
returns: {
label: 'Should Return',
type: String,
viewableBy: ['admins'],
},
description: {
label: 'Description',
type: String,
viewableBy: ['admins'],
},
hooks: {
label: 'Hooks',
type: Array,
viewableBy: ['admins'],
resolveAs: {
type: '[String]',
resolver: callback => {
if (Callbacks[callback.name]) {
const callbacks = Callbacks[callback.name].map(f => f.name);
return callbacks;
} else {
return [];
}
},
},
},
};
export default schema;

View file

@ -2,3 +2,4 @@
import '../components/Emails.jsx';
import '../components/Groups.jsx';
import '../components/Settings.jsx';
import '../components/Callbacks.jsx';

View file

@ -4,6 +4,7 @@ addRoute([
// {name: 'cheatsheet', path: '/cheatsheet', component: import('./components/Cheatsheet.jsx')},
{name: 'groups', path: '/groups', component: () => getDynamicComponent(import('../components/Groups.jsx'))},
{name: 'settings', path: '/settings', componentName: 'Settings'},
{name: 'callbacks', path: '/callbacks', componentName: 'Callbacks'},
// {name: 'emails', path: '/emails', component: () => getDynamicComponent(import('./components/Emails.jsx'))},
{name: 'emails', path: '/emails', componentName: 'Emails'},
]);

View file

@ -1,7 +1,7 @@
Package.describe({
name: "vulcan:debug",
summary: "Vulcan debug package",
version: '1.8.0',
version: '1.8.1',
git: "https://github.com/VulcanJS/Vulcan.git",
debugOnly: true
});
@ -17,8 +17,8 @@ Package.onUse(function (api) {
// Vulcan packages
'vulcan:lib@1.8.0',
'vulcan:email@1.8.0',
'vulcan:lib@1.8.1',
'vulcan:email@1.8.1',
]);

View file

@ -24,7 +24,7 @@ VulcanEmail.addTemplates = templates => {
VulcanEmail.getTemplate = templateName => Handlebars.compile(
VulcanEmail.templates[templateName],
{ noEscape: true}
{ noEscape: true, strict: true}
);
VulcanEmail.buildTemplate = (htmlContent, optionalProperties = {}) => {
@ -46,9 +46,7 @@ VulcanEmail.buildTemplate = (htmlContent, optionalProperties = {}) => {
};
const emailHTML = VulcanEmail.getTemplate("wrapper")(emailProperties);
const inlinedHTML = Juice(emailHTML, {preserveMediaQueries: true});
const doctype = '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">'
return doctype+inlinedHTML;
@ -122,7 +120,7 @@ VulcanEmail.build = async ({ emailName, variables }) => {
const subject = typeof email.subject === 'function' ? email.subject(data) : email.subject;
const html = VulcanEmail.buildTemplate(VulcanEmail.getTemplate(email.template)(data));
const html = VulcanEmail.buildTemplate(VulcanEmail.getTemplate(email.template)(data), data);
return { data, subject, html };
}

View file

@ -10,7 +10,6 @@ Meteor.startup(function () {
Picker.route(email.path, async (params, req, res) => {
let html;
// if email has a custom way of generating test HTML, use it
if (typeof email.getTestHTML !== "undefined") {
@ -20,14 +19,20 @@ Meteor.startup(function () {
// else get test object (sample post, comment, user, etc.)
const testVariables = (typeof email.testVariables === 'function' ? email.testVariables() : email.testVariables) || {};
const result = email.query ? await runQuery(email.query, testVariables) : {data: {}};
// merge test variables with params from URL
const variables = {...testVariables, ...params};
const result = email.query ? await runQuery(email.query, variables) : {data: {}};
// if email has a data() function, merge it with results of query
const emailTestData = email.data ? {...result.data, ...email.data(testVariables)} : result.data;
const emailTestData = email.data ? {...result.data, ...email.data(variables)} : result.data;
const subject = typeof email.subject === 'function' ? email.subject(emailTestData) : email.subject;
const template = VulcanEmail.getTemplate(email.template);
const htmlContent = template(emailTestData)
// then apply email template to properties, and wrap it with buildTemplate
html = VulcanEmail.buildTemplate(VulcanEmail.getTemplate(email.template)(emailTestData));
html = VulcanEmail.buildTemplate(htmlContent, emailTestData);
html += `
<h4 style="margin: 20px;"><code>Subject: ${subject}</code></h4>

View file

@ -1,7 +1,7 @@
Package.describe({
name: "vulcan:email",
summary: "Vulcan email package",
version: '1.8.0',
version: '1.8.1',
git: "https://github.com/VulcanJS/Vulcan.git"
});
@ -10,7 +10,7 @@ Package.onUse(function (api) {
api.versionsFrom('METEOR@1.5.2');
api.use([
'vulcan:lib@1.8.0'
'vulcan:lib@1.8.1'
]);
api.mainModule("lib/server.js", "server");

View file

@ -1,7 +1,7 @@
Package.describe({
name: "vulcan:embed",
summary: "Vulcan Embed package",
version: '1.8.0',
version: '1.8.1',
git: 'https://github.com/VulcanJS/Vulcan.git'
});
@ -10,7 +10,7 @@ Package.onUse( function(api) {
api.versionsFrom('METEOR@1.5.2');
api.use([
'vulcan:core@1.8.0',
'vulcan:core@1.8.1',
'fourseven:scss@4.5.0'
]);

View file

@ -0,0 +1 @@
Vulcan events package, used internally.

View file

@ -0,0 +1,62 @@
import { getSetting } from 'meteor/vulcan:core';
import { addPageFunction, addInitFunction } from 'meteor/vulcan:events';
/*
We provide a special support for Google Analytics.
If you want to enable GA page viewing / tracking, go to
your settings file and update the 'public > googleAnalytics > apiKey'
field with your GA unique identifier (UA-xxx...).
*/
function googleAnaticsTrackPage() {
if (window && window.ga) {
window.ga('send', 'pageview', {
page: window.location.pathname,
});
}
return {};
}
// add client-side callback: log a ga request on page view
addPageFunction(googleAnaticsTrackPage);
function googleAnalyticsInit() {
// get the google analytics id from the settings
const googleAnalyticsId = getSetting('googleAnalytics.apiKey');
// the google analytics id exists & isn't the placeholder from sample_settings.json
if (googleAnalyticsId && googleAnalyticsId !== 'foo123') {
(function(i, s, o, g, r, a, m) {
i['GoogleAnalyticsObject'] = r;
(i[r] =
i[r] ||
function() {
(i[r].q = i[r].q || []).push(arguments);
}),
(i[r].l = 1 * new Date());
(a = s.createElement(o)), (m = s.getElementsByTagName(o)[0]);
a.async = 1;
a.src = g;
m.parentNode.insertBefore(a, m);
})(
window,
document,
'script',
'//www.google-analytics.com/analytics.js',
'ga'
);
const cookieDomain = document.domain === 'localhost' ? 'none' : 'auto';
window.ga('create', googleAnalyticsId, cookieDomain);
// trigger first request once analytics are initialized
googleAnaticsTrackPage();
}
}
// init google analytics on the client module
addInitFunction(googleAnalyticsInit);

View file

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

View file

@ -0,0 +1,3 @@
import { registerSetting } from 'meteor/vulcan:core';
registerSetting('googleAnalytics.apiKey', null, 'Google Analytics ID');

View file

@ -0,0 +1 @@
export * from '../modules/index.js';

View file

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

View file

@ -0,0 +1,21 @@
Intercom package.
### Settings
```
{
"public": {
"intercom": {
"appId": "123foo"
}
},
"intercom": {
"accessToken": "456bar"
}
}
```
Requires installing the [react-intercom](https://github.com/nhagen/react-intercom) package (`npm install --save react-intercom`).

View file

@ -0,0 +1,103 @@
import { getSetting, addCallback, Utils } from 'meteor/vulcan:core';
import { addPageFunction, addInitFunction, addIdentifyFunction, addTrackFunction } from 'meteor/vulcan:events';
/*
Identify User
*/
function intercomIdentify(currentUser) {
intercomSettings = {
app_id: getSetting('intercom.appId'),
name: currentUser.displayName,
email: currentUser.email,
created_at: currentUser.createdAt,
_id: currentUser._id,
pageUrl: currentUser.pageUrl,
};
(function() {
var w = window;
var ic = w.Intercom;
if (typeof ic === 'function') {
ic('reattach_activator');
ic('update', intercomSettings);
} else {
var d = document;
var i = function() {
i.c(arguments);
};
i.q = [];
i.c = function(args) {
i.q.push(args);
};
w.Intercom = i;
function l() {
var s = d.createElement('script');
s.type = 'text/javascript';
s.async = true;
s.src = 'https://widget.intercom.io/widget/icygo7se';
var x = d.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s, x);
}
if (w.attachEvent) {
w.attachEvent('onload', l);
} else {
w.addEventListener('load', l, false);
}
}
})();
}
addIdentifyFunction(intercomIdentify);
/*
Track Event
*/
// function segmentTrack(eventName, eventProperties) {
// analytics.track(eventName, eventProperties);
// }
// addTrackFunction(segmentTrack);
/*
Init Snippet
*/
function intercomInit() {
window.intercomSettings = {
app_id: getSetting('intercom.appId'),
};
(function() {
var w = window;
var ic = w.Intercom;
if (typeof ic === 'function') {
ic('reattach_activator');
ic('update', intercomSettings);
} else {
var d = document;
var i = function() {
i.c(arguments);
};
i.q = [];
i.c = function(args) {
i.q.push(args);
};
w.Intercom = i;
function l() {
var s = d.createElement('script');
s.type = 'text/javascript';
s.async = true;
s.src = 'https://widget.intercom.io/widget/icygo7se';
var x = d.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s, x);
}
if (w.attachEvent) {
w.attachEvent('onload', l);
} else {
w.addEventListener('load', l, false);
}
}
})();
}
addInitFunction(intercomInit);

View file

@ -0,0 +1,3 @@
import './intercom-client.js';
export * from '../modules/index.js';

View file

@ -0,0 +1,49 @@
import Intercom from 'intercom-client';
import { getSetting, addCallback, Utils } from 'meteor/vulcan:core';
import { addPageFunction, addUserFunction, addInitFunction, addIdentifyFunction, addTrackFunction } from 'meteor/vulcan:events';
const token = getSetting('intercom.accessToken');
if (!token) {
throw new Error('Please add your Intercom access token in settings.json');
} else {
const intercomClient = new Intercom.Client({ token });
const getDate = () => new Date().valueOf().toString().substr(0,10);
/*
New User
*/
function intercomNewUser(user) {
intercomClient.users.create({
email: user.email,
custom_attributes: {
name: user.displayName,
profileUrl: Users.getProfileUrl(user, true),
_id: user._id,
}
});
}
addUserFunction(intercomNewUser);
/*
Track Event
*/
function intercomTrackServer(eventName, eventProperties, currentUser) {
intercomClient.events.create({
event_name: eventName,
created_at: getDate(),
email: currentUser.email,
metadata: {
...eventProperties
}
});
}
addTrackFunction(intercomTrackServer);
}

View file

@ -0,0 +1 @@
export * from '../modules/index.js';

View file

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

View file

@ -0,0 +1 @@
Vulcan events package, used internally.

View file

@ -0,0 +1,25 @@
import { addTrackFunction } from 'meteor/vulcan:events';
import { ApolloClient } from 'apollo-client';
import { getRenderContext } from 'meteor/vulcan:lib';
import gql from 'graphql-tag';
function trackInternal(eventName, eventProperties) {
const { apolloClient, store } = getRenderContext();
const mutation = gql`
mutation EventsNew($document: EventsInput) {
EventsNew(document: $document) {
name
createdAt
}
}
`;
const variables = {
document: {
name: eventName,
properties: eventProperties,
},
};
apolloClient.mutate({ mutation, variables });
}
addTrackFunction(trackInternal);

View file

@ -0,0 +1,3 @@
export * from '../modules/index.js';
import './internal-client.js';

View file

@ -0,0 +1,27 @@
import { createCollection, getDefaultResolvers, getDefaultMutations } from 'meteor/vulcan:core';
import schema from './schema.js';
import Users from 'meteor/vulcan:users';
const Events = createCollection({
collectionName: 'Events',
typeName: 'Event',
schema,
resolvers: getDefaultResolvers('Events'),
mutations: getDefaultMutations('Events', {
newCheck: () => true,
editCheck: () => false,
removeCheck: () => false
})
});
Events.checkAccess = (currentUser, doc) => {
return Users.isAdmin(currentUser);
}
export default Events;

View file

@ -0,0 +1 @@
export * from './collection.js';

View file

@ -0,0 +1,38 @@
const schema = {
createdAt: {
type: Date,
optional: true,
onInsert: () => {
return new Date()
}
},
name: {
type: String,
insertableBy: ['guests'],
},
userId: {
type: String,
optional: true,
},
description: {
type: String,
optional: true,
},
unique: {
type: Boolean,
optional: true,
},
important: {
// marking an event as important means it should never be erased
type: Boolean,
optional: true,
},
properties: {
type: Object,
optional: true,
blackbox: true,
insertableBy: ['guests'],
},
};
export default schema;

View file

@ -0,0 +1,19 @@
import { addTrackFunction } from 'meteor/vulcan:events';
import { newMutation } from 'meteor/vulcan:lib';
import Events from '../modules/collection';
async function trackInternalServer(eventName, eventProperties, currentUser) {
const document = {
name: eventName,
properties: eventProperties,
};
return await newMutation({
collection: Events,
document,
currentUser,
validate: false,
context: {},
});
}
addTrackFunction(trackInternalServer);

View file

@ -0,0 +1,3 @@
export * from '../modules/index.js';
import './internal-server';

View file

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

View file

@ -0,0 +1,3 @@
export * from '../modules/index';
import './segment-client.js';

View file

@ -0,0 +1,110 @@
import { getSetting, addCallback, Utils } from 'meteor/vulcan:core';
import {
addPageFunction,
addInitFunction,
addIdentifyFunction,
addTrackFunction,
} from 'meteor/vulcan:events';
/*
Track Page
*/
function segmentTrackPage(route) {
const { name, path } = route;
const properties = {
url: Utils.getSiteUrl().slice(0, -1) + path,
path,
};
window.analytics.page(null, name, properties);
return {};
}
addPageFunction(segmentTrackPage);
/*
Identify User
*/
function segmentIdentify(currentUser) {
window.analytics.identify(currentUser._id, {
email: currentUser.email,
pageUrl: currentUser.pageUrl,
});
}
addIdentifyFunction(segmentIdentify);
/*
Track Event
*/
function segmentTrack(eventName, eventProperties) {
analytics.track(eventName, eventProperties);
}
addTrackFunction(segmentTrack);
/*
Init Snippet
*/
function segmentInit() {
!(function() {
var analytics = (window.analytics = window.analytics || []);
if (!analytics.initialize)
if (analytics.invoked)
window.console &&
console.error &&
console.error('Segment snippet included twice.');
else {
analytics.invoked = !0;
analytics.methods = [
'trackSubmit',
'trackClick',
'trackLink',
'trackForm',
'pageview',
'identify',
'reset',
'group',
'track',
'ready',
'alias',
'debug',
'page',
'once',
'off',
'on',
];
analytics.factory = function(t) {
return function() {
var e = Array.prototype.slice.call(arguments);
e.unshift(t);
analytics.push(e);
return analytics;
};
};
for (var t = 0; t < analytics.methods.length; t++) {
var e = analytics.methods[t];
analytics[e] = analytics.factory(e);
}
analytics.load = function(t) {
var e = document.createElement('script');
e.type = 'text/javascript';
e.async = !0;
e.src =
('https:' === document.location.protocol ? 'https://' : 'http://') +
'cdn.segment.com/analytics.js/v1/' +
t +
'/analytics.min.js';
var n = document.getElementsByTagName('script')[0];
n.parentNode.insertBefore(e, n);
};
analytics.SNIPPET_VERSION = '4.0.0';
analytics.load(getSetting('segment.clientKey'));
}
})();
}
addInitFunction(segmentInit);

View file

@ -0,0 +1,4 @@
import { registerSetting } from 'meteor/vulcan:core';
registerSetting('segment.clientKey', null, 'Segment client-side API key');
registerSetting('segment.serverKey', null, 'Segment server-side API key');

View file

@ -0,0 +1,3 @@
// export * from '../modules/index';
export * from './segment-server.js';

View file

@ -0,0 +1,40 @@
import Analytics from 'analytics-node';
import { getSetting, addCallback, Utils } from 'meteor/vulcan:core';
import { addPageFunction, addInitFunction, addIdentifyFunction, addTrackFunction } from 'meteor/vulcan:events';
const segmentWriteKey = getSetting('segment.serverKey');
if (segmentWriteKey) {
const analytics = new Analytics(segmentWriteKey);
/*
Identify User
*/
function segmentIdentifyServer(currentUser) {
analytics.identify({
userId: currentUser._id,
traits: {
email: currentUser.email,
pageUrl: currentUser.pageUrl,
},
});
}
addIdentifyFunction(segmentIdentifyServer);
/*
Track Event
*/
function segmentTrackServer(eventName, eventProperties, currentUser) {
analytics.track({
event: eventName,
properties: eventProperties,
userId: currentUser && currentUser._id,
});
}
addTrackFunction(segmentTrackServer);
}

View file

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

View file

@ -1,5 +0,0 @@
import { addCallback } from 'meteor/vulcan:core';
import { sendGoogleAnalyticsRequest } from './helpers';
// add client-side callback: log a ga request on page view
addCallback('router.onUpdate', sendGoogleAnalyticsRequest);

View file

@ -1,8 +0,0 @@
import Events from './collection.js';
import { initGoogleAnalytics } from './helpers.js';
import './callbacks.js';
// init google analytics on the client module
initGoogleAnalytics();
export default Events;

View file

@ -0,0 +1 @@
export * from '../modules/index.js';

View file

@ -1,33 +0,0 @@
import SimpleSchema from 'simpl-schema';
const Events = new Mongo.Collection('events');
Events.schema = new SimpleSchema({
createdAt: {
type: Date
},
name: {
type: String
},
description: {
type: String,
optional: true
},
unique: {
type: Boolean,
optional: true
},
important: { // marking an event as important means it should never be erased
type: Boolean,
optional: true
},
properties: {
type: Object,
optional: true,
blackbox: true
}
});
Events.attachSchema(Events.schema);
export default Events;

View file

@ -1,60 +0,0 @@
import { getSetting, registerSetting } from 'meteor/vulcan:core';
import Events from './collection.js';
registerSetting('googleAnalyticsId', null, 'Google Analytics ID');
/*
We provide a special support for Google Analytics.
If you want to enable GA page viewing / tracking, go to
your settings file and update the 'public > googleAnalyticsId'
field with your GA unique identifier (UA-xxx...).
*/
export function sendGoogleAnalyticsRequest () {
if (window && window.ga) {
window.ga('send', 'pageview', {
'page': window.location.pathname
});
}
return {}
}
export const initGoogleAnalytics = () => {
// get the google analytics id from the settings
const googleAnalyticsId = getSetting('googleAnalyticsId');
// the google analytics id exists & isn't the placeholder from sample_settings.json
if (googleAnalyticsId && googleAnalyticsId !== 'foo123') {
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
const cookieDomain = document.domain === 'localhost' ? 'none' : 'auto';
window.ga('create', googleAnalyticsId, cookieDomain);
// trigger first request once analytics are initialized
sendGoogleAnalyticsRequest();
}
};
// collection based logging
Events.log = function (event) {
// if event is supposed to be unique, check if it has already been logged
if (!!event.unique && !!Events.findOne({name: event.name})) {
return;
}
event.createdAt = new Date();
Events.insert(event);
};

View file

@ -0,0 +1,41 @@
import { addCallback } from 'meteor/vulcan:core';
export const initFunctions = [];
export const trackFunctions = [];
export const addInitFunction = f => {
initFunctions.push(f);
// execute init function as soon as possible
f();
};
export const addTrackFunction = f => {
trackFunctions.push(f);
};
export const track = async (eventName, eventProperties, currentUser) => {
for (let f of trackFunctions) {
await f(eventName, eventProperties, currentUser);
}
};
export const addUserFunction = f => {
addCallback('users.new.async', f);
};
export const addIdentifyFunction = f => {
addCallback('events.identify', f);
};
export const addPageFunction = f => {
const f2 = (empty, route) => f(route);
// rename f2 to same name as f
// see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
const descriptor = Object.create(null); // no inherited properties
descriptor.value = f.name;
Object.defineProperty(f2, 'name', descriptor);
addCallback('router.onUpdate', f2);
};

View file

@ -0,0 +1 @@
export * from './events';

View file

@ -1,18 +0,0 @@
// import { GraphQLSchema } from 'meteor/vulcan:core';
// // import Events from './collection.js';
// import { requestAnalyticsAsync } from './helpers.js';
// GraphQLSchema.addMutation('eventTrack(eventName: String, properties: JSON): JSON');
// const resolvers = {
// Mutation: {
// eventTrack: (root, { eventName, properties }, context) => {
// const user = context.currentUser || {_id: 'anonymous'};
// return properties;
// },
// },
// };
// GraphQLSchema.addResolvers(resolvers);

View file

@ -1,5 +0,0 @@
import Events from './collection.js';
import './callbacks.js';
// import './mutations.js';
export default Events;

View file

@ -0,0 +1 @@
export * from '../modules/index.js';

View file

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

View file

@ -1,7 +1,7 @@
Package.describe({
name: "vulcan:forms-tags",
summary: "Vulcan tag input package",
version: '1.8.0',
version: '1.8.1',
git: 'https://github.com/VulcanJS/Vulcan.git'
});
@ -10,8 +10,8 @@ Package.onUse( function(api) {
api.versionsFrom('METEOR@1.5.2');
api.use([
'vulcan:core@1.8.0',
'vulcan:forms@1.8.0'
'vulcan:core@1.8.1',
'vulcan:forms@1.8.1'
]);
api.mainModule("lib/export.js", ["client", "server"]);

View file

@ -169,15 +169,15 @@ class Upload extends PureComponent {
const newValue = this.enableMultiple() ? removeNthItem(this.state.value, index): '';
this.context.addToAutofilledValues({[this.props.name]: newValue});
this.setState({
preview: newValue,
preview: null,
value: newValue,
});
}
render() {
const { uploading, preview, value } = this.state;
// show the actual uploaded image or the preview
const imageData = this.enableMultiple() ? (preview ? value.concat(preview) : value) : value || preview;
return (

View file

@ -1,7 +1,7 @@
Package.describe({
name: "vulcan:forms-upload",
summary: "Vulcan package extending vulcan:forms to upload images to Cloudinary from a drop zone.",
version: "1.8.0",
version: "1.8.1",
git: 'https://github.com/xavcz/nova-forms-upload.git'
});
@ -10,8 +10,8 @@ Package.onUse( function(api) {
api.versionsFrom('METEOR@1.5.2');
api.use([
'vulcan:core@1.8.0',
'vulcan:forms@1.8.0',
'vulcan:core@1.8.1',
'vulcan:forms@1.8.1',
'fourseven:scss@4.5.0'
]);

View file

@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import Alert from 'react-bootstrap/lib/Alert'
import { registerComponent } from 'meteor/vulcan:core';
const Flash = ({message, type}) => {
@ -24,4 +25,4 @@ Flash.propTypes = {
message: PropTypes.oneOfType([PropTypes.object.isRequired, PropTypes.array.isRequired])
}
export default Flash;
registerComponent('FormFlash', Flash);

View file

@ -25,12 +25,9 @@ This component expects:
import { Components, Utils, runCallbacks } from 'meteor/vulcan:core';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, intlShape } from 'meteor/vulcan:i18n';
import { intlShape } from 'meteor/vulcan:i18n';
import Formsy from 'formsy-react';
import Button from 'react-bootstrap/lib/Button';
import Flash from "./Flash.jsx";
import FormGroup from "./FormGroup.jsx";
import { flatten, deepValue, getEditableFields, getInsertableFields } from '../modules/utils.js';
import { getEditableFields, getInsertableFields } from '../modules/utils.js';
/*
@ -156,21 +153,32 @@ class Form extends Component {
}
// replace empty value, which has not been prefilled, by the default value from the schema
if (fieldSchema.defaultValue && field.value === "") {
// keep defaultValue for backwards compatibility even though it doesn't actually work
if (fieldSchema.defaultValue && (typeof field.value === 'undefined' || field.value === '')) {
field.value = fieldSchema.defaultValue;
}
if (fieldSchema.default && (typeof field.value === 'undefined' || field.value === '')) {
field.value = fieldSchema.default;
}
// add options if they exist
if (fieldSchema.form && fieldSchema.form.options) {
field.options = typeof fieldSchema.form.options === "function" ? fieldSchema.form.options.call(fieldSchema, this.props) : fieldSchema.form.options;
// in case of checkbox groups, check "checked" option to populate value
if (!field.value) {
field.value = _.where(field.options, {checked: true}).map(option => option.value);
}
}
if (fieldSchema.form && fieldSchema.form.disabled) {
field.disabled = typeof fieldSchema.form.disabled === "function" ? fieldSchema.form.disabled.call(fieldSchema) : fieldSchema.form.disabled;
}
if (fieldSchema.form && fieldSchema.form.help) {
field.help = typeof fieldSchema.form.help === "function" ? fieldSchema.form.help.call(fieldSchema) : fieldSchema.form.help;
if (fieldSchema.form) {
for (const prop in fieldSchema.form) {
if (prop !== 'prefill' && prop !== 'options' && fieldSchema.form.hasOwnProperty(prop)) {
field[prop] = typeof fieldSchema.form[prop] === "function" ?
fieldSchema.form[prop].call(fieldSchema) :
fieldSchema.form[prop];
}
}
}
// add limit
@ -353,7 +361,8 @@ class Form extends Component {
message = error.data.errors.map(error => {
return {
content: this.getErrorMessage(error)
content: this.getErrorMessage(error),
data: error.data,
}
});
@ -362,8 +371,8 @@ class Form extends Component {
message = {content: error.message || this.context.intl.formatMessage({id: error.id, defaultMessage: error.id}, error.data)}
}
return <Flash key={index} message={message} type="error"/>
return <Components.FormFlash key={index} message={message} type="error"/>;
})}
</div>
)
@ -613,26 +622,25 @@ class Form extends Component {
disabled={this.state.disabled}
ref="form"
>
{this.renderErrors()}
{fieldGroups.map(group => <FormGroup key={group.name} {...group} updateCurrentValues={this.updateCurrentValues} />)}
<div className="form-submit">
<Button type="submit" bsStyle="primary">{this.props.submitLabel ? this.props.submitLabel : <FormattedMessage id="forms.submit"/>}</Button>
{this.props.cancelCallback ? <a className="form-cancel" onClick={(e) => {e.preventDefault(); this.props.cancelCallback(this.getDocument())}}>{this.props.cancelLabel ? this.props.cancelLabel : <FormattedMessage id="forms.cancel"/>}</a> : null}
</div>
{this.renderErrors()}
{fieldGroups.map(group => <Components.FormGroup key={group.name} {...group} updateCurrentValues={this.updateCurrentValues} />)}
{this.props.repeatErrors && this.renderErrors()}
<Components.FormSubmit submitLabel={this.props.submitLabel}
cancelLabel={this.props.cancelLabel}
cancelCallback={this.props.cancelCallback}
document={this.getDocument()}
deleteDocument={(this.props.formType === 'edit'
&& this.props.showRemove
&& this.deleteDocument)
|| null}
collectionName={collectionName}
/>
</Formsy.Form>
{
this.props.formType === 'edit' && this.props.showRemove
? <div>
<hr/>
<a href="javascript:void()" onClick={this.deleteDocument} className={`delete-link ${collectionName}-delete-link`}>
<Components.Icon name="close"/> <FormattedMessage id="forms.delete"/>
</a>
</div>
: null
}
</div>
)
}
@ -660,6 +668,7 @@ Form.propTypes = {
showRemove: PropTypes.bool,
submitLabel: PropTypes.string,
cancelLabel: PropTypes.string,
repeatErrors: PropTypes.bool,
// callbacks
submitCallback: PropTypes.func,
@ -673,7 +682,8 @@ Form.propTypes = {
}
Form.defaultProps = {
layout: "horizontal",
layout: 'horizontal',
repeatErrors: false,
}
Form.contextTypes = {

View file

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { intlShape } from 'meteor/vulcan:i18n';
import classNames from 'classnames';
import { Components } from 'meteor/vulcan:core';
import { registerComponent } from 'meteor/vulcan:core';
class FormComponent extends PureComponent {
@ -97,6 +98,9 @@ class FormComponent extends PureComponent {
case 'datetime':
return <Components.FormComponentDateTime {...properties} />;
case 'time':
return <Components.FormComponentTime {...properties} />;
case 'text':
return <Components.FormComponentDefault {...properties}/>;
@ -120,16 +124,37 @@ class FormComponent extends PureComponent {
)
}
showClear = () => {
return ['datetime', 'select', 'radiogroup'].includes(this.props.control);
}
clearField = (e) => {
e.preventDefault();
console.log(this.props)
const fieldName = this.props.name;
// clear value
this.props.updateCurrentValues({[fieldName]: null});
// add it to unset
this.context.addToDeletedValues(fieldName);
}
renderClear() {
return (
<a href="javascript:void(0)" className="form-component-clear" title={this.context.intl.formatMessage({id: 'forms.clear_field'})} onClick={this.clearField}><span></span></a>
)
}
render() {
const hasErrors = this.props.errors && this.props.errors.length;
const inputClass = classNames('form-input', `input-${this.props.name}`, {'input-error': hasErrors});
const inputClass = classNames('form-input', `input-${this.props.name}`, `form-component-${this.props.control || 'default'}`,{'input-error': hasErrors});
return (
<div className={inputClass}>
{this.props.beforeComponent ? this.props.beforeComponent : null}
{this.renderComponent()}
{hasErrors ? this.renderErrors() : null}
{this.showClear() ? this.renderClear() : null}
{this.props.limit ? <div className={classNames('form-control-limit', {danger: this.state.limit < 10})}>{this.state.limit}</div> : null}
{this.props.afterComponent ? this.props.afterComponent : null}
</div>
@ -153,7 +178,8 @@ FormComponent.propTypes = {
}
FormComponent.contextTypes = {
intl: intlShape
intl: intlShape,
addToDeletedValues: PropTypes.func,
};
export default FormComponent;
registerComponent('FormComponent', FormComponent);

View file

@ -1,8 +1,8 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import FormComponent from './FormComponent.jsx';
import { Components } from 'meteor/vulcan:core';
import classNames from 'classnames';
import { registerComponent } from 'meteor/vulcan:core';
class FormGroup extends PureComponent {
@ -40,7 +40,7 @@ class FormGroup extends PureComponent {
<div className="form-section">
{this.props.name === 'default' ? null : this.renderHeading()}
<div className={classNames({'form-section-collapsed': this.state.collapsed && !hasErrors})}>
{this.props.fields.map(field => <FormComponent key={field.name} {...field} updateCurrentValues={this.props.updateCurrentValues} />)}
{this.props.fields.map(field => <Components.FormComponent key={field.name} {...field} updateCurrentValues={this.props.updateCurrentValues} />)}
</div>
</div>
)
@ -55,4 +55,4 @@ FormGroup.propTypes = {
updateCurrentValues: PropTypes.func
}
export default FormGroup;
registerComponent('FormGroup', FormGroup);

View file

@ -0,0 +1,65 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Components } from 'meteor/vulcan:core';
import { registerComponent } from 'meteor/vulcan:core';
import Button from 'react-bootstrap/lib/Button';
import { FormattedMessage } from 'meteor/vulcan:i18n';
const FormSubmit = ({
submitLabel,
cancelLabel,
cancelCallback,
document,
deleteDocument,
collectionName,
classes
}) => (
<div className="form-submit">
<Button type="submit" bsStyle="primary">
{submitLabel ? submitLabel : <FormattedMessage id="forms.submit"/>}
</Button>
{
cancelCallback
?
<a className="form-cancel" onClick={(e) => {
e.preventDefault();
cancelCallback(document);
}}>{cancelLabel ? cancelLabel :
<FormattedMessage id="forms.cancel"/>}</a>
:
null
}
{
deleteDocument
?
<div>
<hr/>
<a href="javascript:void()" onClick={deleteDocument}
className={`delete-link ${collectionName}-delete-link`}>
<Components.Icon name="close"/> <FormattedMessage id="forms.delete"/>
</a>
</div>
:
null
}
</div>
);
FormSubmit.propTypes = {
submitLabel: PropTypes.string,
cancelLabel: PropTypes.string,
cancelCallback: PropTypes.func,
document: PropTypes.object,
deleteDocument: PropTypes.func,
collectionName: PropTypes.string,
classes: PropTypes.object,
};
registerComponent('FormSubmit', FormSubmit);

View file

@ -35,6 +35,13 @@ import { withDocument } from 'meteor/vulcan:core';
class FormWrapper extends PureComponent {
constructor(props) {
super(props);
// instantiate the wrapped component in constructor, not in render
// see https://reactjs.org/docs/higher-order-components.html#dont-use-hocs-inside-the-render-method
this.FormComponent = this.getComponent();
}
// return the current schema based on either the schema or collection prop
getSchema() {
return this.props.schema ? this.props.schema : Utils.stripTelescopeNamespace(this.props.collection.simpleSchema()._schema);
@ -69,7 +76,7 @@ class FormWrapper extends PureComponent {
mutationFields = _.intersection(mutationFields, fields);
}
// resolve any array field with resolveAs as fieldName{_id}
// resolve any array field with resolveAs as fieldName{_id} -> why?
/*
- string field with no resolver -> fieldName
- string field with a resolver -> fieldName
@ -77,9 +84,9 @@ class FormWrapper extends PureComponent {
- array field with a resolver -> fieldName{_id}
*/
const mapFieldNameToField = fieldName => {
const field = this.getSchema()[fieldName]
const field = this.getSchema()[fieldName];
return field.resolveAs && field.type.definitions[0].type === Array
? `${fieldName}{_id}` // if it's a custom resolver, add a basic query to its _id
? `${fieldName}` // if it's a custom resolver, add a basic query to its _id
: fieldName; // else just ask for the field name
}
queryFields = queryFields.map(mapFieldNameToField);
@ -108,13 +115,7 @@ class FormWrapper extends PureComponent {
};
}
shouldComponentUpdate(nextProps) {
// prevent extra re-renderings for unknown reasons
// re-render only if the document selector changes
return nextProps.slug !== this.props.slug || nextProps.documentId !== this.props.documentId;
}
render() {
getComponent() {
// console.log(this)
@ -136,6 +137,8 @@ class FormWrapper extends PureComponent {
queryName: `${prefix}FormQuery`,
collection: this.props.collection,
fragment: this.getFragments().queryFragment,
fetchPolicy: 'network-only', // we always want to load a fresh copy of the document
enableCache: false,
};
// options for withNew, withEdit, and withRemove HoCs
@ -180,6 +183,16 @@ class FormWrapper extends PureComponent {
}
}
shouldComponentUpdate(nextProps) {
// prevent extra re-renderings for unknown reasons
// re-render only if the document selector changes
return nextProps.slug !== this.props.slug || nextProps.documentId !== this.props.documentId;
}
render() {
return this.FormComponent;
}
}
FormWrapper.propTypes = {

View file

@ -0,0 +1,73 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import DateTimePicker from 'react-datetime';
import { registerComponent } from 'meteor/vulcan:core';
class Time extends PureComponent {
constructor(props) {
super(props);
this.updateDate = this.updateDate.bind(this);
}
// when the datetime picker has mounted, SmartForm will catch the date value (no formsy mixin in this component)
componentDidMount() {
if (this.props.value) {
this.context.updateCurrentValues({[this.props.name]: this.props.value});
}
}
updateDate(mDate) {
// if this is a properly formatted moment date, update time
if (typeof mDate === 'object') {
this.context.updateCurrentValues({[this.props.name]: mDate.format('HH:mm')});
}
}
render() {
const date = new Date();
// transform time string into date object to work inside datetimepicker
const time = this.props.value;
if (time) {
date.setHours(parseInt(time.substr(0,2)), parseInt(time.substr(3,5)));
} else {
date.setHours(0,0);
}
return (
<div className="form-group row">
<label className="control-label col-sm-3">{this.props.label}</label>
<div className="col-sm-9">
<DateTimePicker
value={date}
viewMode="time"
dateFormat={false}
timeFormat="HH:mm"
// newDate argument is a Moment object given by react-datetime
onChange={newDate => this.updateDate(newDate)}
inputProps={{name: this.props.name}}
/>
</div>
</div>
);
}
}
Time.propTypes = {
control: PropTypes.any,
datatype: PropTypes.any,
group: PropTypes.any,
label: PropTypes.string,
name: PropTypes.string,
value: PropTypes.any,
};
Time.contextTypes = {
updateCurrentValues: PropTypes.func,
};
export default Time;
registerComponent('FormComponentTime', Time);

View file

@ -7,6 +7,11 @@ import '../components/bootstrap/Number.jsx';
import '../components/bootstrap/Radiogroup.jsx';
import '../components/bootstrap/Select.jsx';
import '../components/bootstrap/Textarea.jsx';
import '../components/bootstrap/Time.jsx';
import '../components/bootstrap/Url.jsx';
import '../components/Flash.jsx';
import '../components/FormComponent.jsx';
import '../components/FormGroup.jsx';
import '../components/FormSubmit.jsx';
import '../components/FormWrapper.jsx';

View file

@ -1,4 +1,5 @@
$light-grey: #ddd;
$medium-grey: #bbb;
$vmargin: 15px;
$light-border: $light-grey;
@ -181,4 +182,35 @@ div.ReactTags__suggestions mark{
li{
margin: 0;
}
}
.form-component-select, .form-component-datetime{
.col-sm-9{
padding-right: 40px;
}
}
.form-component-clear{
position: absolute;
top: 11px;
right: 0px;
background: $light-grey;
color: #fff;
border-radius: 100%;
height: 16px;
width: 16px;
border: 0;
display: flex;
justify-content: center;
align-items: center;
span{
font-size: 8px;
display: block;
line-height: 1;
}
&:hover{
text-decoration: none;
background: $medium-grey;
color: #fff;
}
}

View file

@ -1,7 +1,7 @@
Package.describe({
name: "vulcan:forms",
summary: "Form containers for React",
version: '1.8.0',
version: '1.8.1',
git: "https://github.com/meteor-utilities/react-form-containers.git"
});
@ -10,7 +10,7 @@ Package.onUse(function(api) {
api.versionsFrom("METEOR@1.3");
api.use([
'vulcan:core@1.8.0',
'vulcan:core@1.8.1',
'fourseven:scss@4.5.0'
]);

View file

@ -1,7 +1,7 @@
Package.describe({
name: "vulcan:i18n-en-us",
summary: "Vulcan i18n package (en_US)",
version: '1.8.0',
version: '1.8.1',
git: "https://github.com/VulcanJS/Vulcan.git"
});
@ -10,7 +10,7 @@ Package.onUse(function (api) {
api.versionsFrom('METEOR@1.5.2');
api.use([
'vulcan:core@1.8.0'
'vulcan:core@1.8.1'
]);
api.addFiles([

View file

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

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