mirror of
https://github.com/vale981/Vulcan
synced 2025-03-04 09:11:43 -05:00
Merge branch 'devel'
This commit is contained in:
commit
867495b8ec
138 changed files with 3053 additions and 666 deletions
|
@ -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
|
||||
|
|
|
@ -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
7
.meteor/.id
Normal 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
|
|
@ -1 +1 @@
|
|||
METEOR@1.5.2.2
|
||||
METEOR@1.6
|
||||
|
|
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal 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
549
package-lock.json
generated
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
13
package.json
13
package.json
|
@ -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",
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -25,6 +25,12 @@ registerFragment(`
|
|||
...UsersMinimumInfo
|
||||
}
|
||||
}
|
||||
# vulcan:voting
|
||||
# voting
|
||||
currentUserVotes{
|
||||
...VoteFragment
|
||||
}
|
||||
baseScore
|
||||
score
|
||||
}
|
||||
`);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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}`),
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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',
|
||||
|
||||
]);
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
26
packages/vulcan-core/lib/modules/components/RouterHook.jsx
Normal file
26
packages/vulcan-core/lib/modules/components/RouterHook.jsx
Normal 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);
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.`
|
||||
});
|
||||
}
|
|
@ -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();
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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');
|
||||
|
|
31
packages/vulcan-debug/lib/components/Callbacks.jsx
Normal file
31
packages/vulcan-debug/lib/components/Callbacks.jsx
Normal 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;
|
|
@ -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>
|
||||
|
|
19
packages/vulcan-debug/lib/modules/callbacks/collection.js
Normal file
19
packages/vulcan-debug/lib/modules/callbacks/collection.js
Normal 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;
|
12
packages/vulcan-debug/lib/modules/callbacks/fragments.js
Normal file
12
packages/vulcan-debug/lib/modules/callbacks/fragments.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { registerFragment } from 'meteor/vulcan:lib';
|
||||
|
||||
registerFragment(`
|
||||
fragment CallbacksFragment on Callback {
|
||||
name
|
||||
arguments
|
||||
runs
|
||||
returns
|
||||
description
|
||||
hooks
|
||||
}
|
||||
`);
|
26
packages/vulcan-debug/lib/modules/callbacks/resolvers.js
Normal file
26
packages/vulcan-debug/lib/modules/callbacks/resolvers.js
Normal 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;
|
57
packages/vulcan-debug/lib/modules/callbacks/schema.js
Normal file
57
packages/vulcan-debug/lib/modules/callbacks/schema.js
Normal 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;
|
|
@ -2,3 +2,4 @@
|
|||
import '../components/Emails.jsx';
|
||||
import '../components/Groups.jsx';
|
||||
import '../components/Settings.jsx';
|
||||
import '../components/Callbacks.jsx';
|
||||
|
|
|
@ -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'},
|
||||
]);
|
|
@ -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',
|
||||
|
||||
]);
|
||||
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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'
|
||||
]);
|
||||
|
||||
|
|
1
packages/vulcan-events-ga/README.md
Normal file
1
packages/vulcan-events-ga/README.md
Normal file
|
@ -0,0 +1 @@
|
|||
Vulcan events package, used internally.
|
62
packages/vulcan-events-ga/lib/client/ga.js
Normal file
62
packages/vulcan-events-ga/lib/client/ga.js
Normal 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);
|
2
packages/vulcan-events-ga/lib/client/main.js
Normal file
2
packages/vulcan-events-ga/lib/client/main.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
import './ga.js';
|
||||
export * from '../modules/index.js';
|
3
packages/vulcan-events-ga/lib/modules/index.js
Normal file
3
packages/vulcan-events-ga/lib/modules/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { registerSetting } from 'meteor/vulcan:core';
|
||||
|
||||
registerSetting('googleAnalytics.apiKey', null, 'Google Analytics ID');
|
1
packages/vulcan-events-ga/lib/server/main.js
Normal file
1
packages/vulcan-events-ga/lib/server/main.js
Normal file
|
@ -0,0 +1 @@
|
|||
export * from '../modules/index.js';
|
20
packages/vulcan-events-ga/package.js
Normal file
20
packages/vulcan-events-ga/package.js
Normal 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');
|
||||
|
||||
});
|
21
packages/vulcan-events-intercom/README.md
Normal file
21
packages/vulcan-events-intercom/README.md
Normal 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`).
|
103
packages/vulcan-events-intercom/lib/client/intercom-client.js
Normal file
103
packages/vulcan-events-intercom/lib/client/intercom-client.js
Normal 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);
|
3
packages/vulcan-events-intercom/lib/client/main.js
Normal file
3
packages/vulcan-events-intercom/lib/client/main.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import './intercom-client.js';
|
||||
|
||||
export * from '../modules/index.js';
|
0
packages/vulcan-events-intercom/lib/modules/index.js
Normal file
0
packages/vulcan-events-intercom/lib/modules/index.js
Normal 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);
|
||||
|
||||
}
|
1
packages/vulcan-events-intercom/lib/server/main.js
Normal file
1
packages/vulcan-events-intercom/lib/server/main.js
Normal file
|
@ -0,0 +1 @@
|
|||
export * from '../modules/index.js';
|
20
packages/vulcan-events-intercom/package.js
Normal file
20
packages/vulcan-events-intercom/package.js
Normal 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");
|
||||
|
||||
});
|
1
packages/vulcan-events-internal/README.md
Normal file
1
packages/vulcan-events-internal/README.md
Normal file
|
@ -0,0 +1 @@
|
|||
Vulcan events package, used internally.
|
|
@ -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);
|
3
packages/vulcan-events-internal/lib/client/main.js
Normal file
3
packages/vulcan-events-internal/lib/client/main.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from '../modules/index.js';
|
||||
|
||||
import './internal-client.js';
|
27
packages/vulcan-events-internal/lib/modules/collection.js
Normal file
27
packages/vulcan-events-internal/lib/modules/collection.js
Normal 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;
|
1
packages/vulcan-events-internal/lib/modules/index.js
Normal file
1
packages/vulcan-events-internal/lib/modules/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './collection.js';
|
38
packages/vulcan-events-internal/lib/modules/schema.js
Normal file
38
packages/vulcan-events-internal/lib/modules/schema.js
Normal 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;
|
|
@ -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);
|
3
packages/vulcan-events-internal/lib/server/main.js
Normal file
3
packages/vulcan-events-internal/lib/server/main.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from '../modules/index.js';
|
||||
|
||||
import './internal-server';
|
20
packages/vulcan-events-internal/package.js
Normal file
20
packages/vulcan-events-internal/package.js
Normal 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');
|
||||
|
||||
});
|
3
packages/vulcan-events-segment/lib/client/main.js
Normal file
3
packages/vulcan-events-segment/lib/client/main.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from '../modules/index';
|
||||
|
||||
import './segment-client.js';
|
110
packages/vulcan-events-segment/lib/client/segment-client.js
Normal file
110
packages/vulcan-events-segment/lib/client/segment-client.js
Normal 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);
|
4
packages/vulcan-events-segment/lib/modules/index.js
Normal file
4
packages/vulcan-events-segment/lib/modules/index.js
Normal 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');
|
3
packages/vulcan-events-segment/lib/server/main.js
Normal file
3
packages/vulcan-events-segment/lib/server/main.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
// export * from '../modules/index';
|
||||
|
||||
export * from './segment-server.js';
|
40
packages/vulcan-events-segment/lib/server/segment-server.js
Normal file
40
packages/vulcan-events-segment/lib/server/segment-server.js
Normal 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);
|
||||
}
|
20
packages/vulcan-events-segment/package.js
Normal file
20
packages/vulcan-events-segment/package.js
Normal 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');
|
||||
|
||||
});
|
|
@ -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);
|
|
@ -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;
|
1
packages/vulcan-events/lib/client/main.js
Normal file
1
packages/vulcan-events/lib/client/main.js
Normal file
|
@ -0,0 +1 @@
|
|||
export * from '../modules/index.js';
|
|
@ -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;
|
|
@ -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);
|
||||
|
||||
};
|
41
packages/vulcan-events/lib/modules/events.js
Normal file
41
packages/vulcan-events/lib/modules/events.js
Normal 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);
|
||||
};
|
1
packages/vulcan-events/lib/modules/index.js
Normal file
1
packages/vulcan-events/lib/modules/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './events';
|
|
@ -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);
|
|
@ -1,5 +0,0 @@
|
|||
import Events from './collection.js';
|
||||
import './callbacks.js';
|
||||
// import './mutations.js';
|
||||
|
||||
export default Events;
|
1
packages/vulcan-events/lib/server/main.js
Normal file
1
packages/vulcan-events/lib/server/main.js
Normal file
|
@ -0,0 +1 @@
|
|||
export * from '../modules/index.js';
|
|
@ -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');
|
||||
|
||||
});
|
||||
|
|
|
@ -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"]);
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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'
|
||||
]);
|
||||
|
||||
|
|
|
@ -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);
|
|
@ -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 = {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
65
packages/vulcan-forms/lib/components/FormSubmit.jsx
Normal file
65
packages/vulcan-forms/lib/components/FormSubmit.jsx
Normal 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);
|
|
@ -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 = {
|
||||
|
|
73
packages/vulcan-forms/lib/components/bootstrap/Time.jsx
Normal file
73
packages/vulcan-forms/lib/components/bootstrap/Time.jsx
Normal 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);
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
]);
|
||||
|
|
|
@ -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([
|
||||
|
|
|
@ -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
Loading…
Add table
Reference in a new issue