mirror of
https://github.com/vale981/Vulcan
synced 2025-03-06 01:51:40 -05:00
commit
94d6c29db6
192 changed files with 9815 additions and 1139 deletions
|
@ -10,7 +10,7 @@ indent_brace_style = 1TBS
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
indent_style = space
|
indent_style = space
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
max_line_length = 80
|
max_line_length = 120
|
||||||
quote_type = auto
|
quote_type = auto
|
||||||
spaces_around_operators = true
|
spaces_around_operators = true
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
|
@ -50,10 +50,15 @@
|
||||||
"meteor": true,
|
"meteor": true,
|
||||||
"node": true
|
"node": true
|
||||||
},
|
},
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"modules": true,
|
||||||
|
"jsx": true
|
||||||
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"babel",
|
"babel",
|
||||||
"meteor",
|
"meteor",
|
||||||
"react"
|
"react",
|
||||||
|
"prettier"
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
"import/resolver": "meteor"
|
"import/resolver": "meteor"
|
||||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -48,3 +48,5 @@ jsdoc-conf.json
|
||||||
jsdoc.json
|
jsdoc.json
|
||||||
packages/nova-router/.npm
|
packages/nova-router/.npm
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
|
|
||||||
|
typings
|
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
|
|
@ -14,6 +14,9 @@ accounts-password@1.4.0
|
||||||
|
|
||||||
############ Your Packages ############
|
############ Your Packages ############
|
||||||
|
|
||||||
|
#tutorial-step-1 - You can follow along with the video here: http://docs.vulcanjs.org/example-simple.html
|
||||||
|
# This is where you enable packages.
|
||||||
|
|
||||||
example-simple
|
example-simple
|
||||||
# example-movies
|
# example-movies
|
||||||
# example-instagram
|
# example-instagram
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
METEOR@1.5.2.2
|
METEOR@1.6
|
||||||
|
|
|
@ -1,58 +1,58 @@
|
||||||
accounts-base@1.3.4
|
accounts-base@1.4.2
|
||||||
accounts-password@1.4.0
|
accounts-password@1.5.0
|
||||||
allow-deny@1.0.9
|
allow-deny@1.1.0
|
||||||
autoupdate@1.3.12
|
autoupdate@1.3.12
|
||||||
babel-compiler@6.20.0
|
babel-compiler@6.24.7
|
||||||
babel-runtime@1.0.1
|
babel-runtime@1.1.1
|
||||||
base64@1.0.10
|
base64@1.0.10
|
||||||
binary-heap@1.0.10
|
binary-heap@1.0.10
|
||||||
boilerplate-generator@1.2.0
|
boilerplate-generator@1.3.0
|
||||||
buffer@0.0.0
|
buffer@0.0.0
|
||||||
caching-compiler@1.1.9
|
caching-compiler@1.1.9
|
||||||
callback-hook@1.0.10
|
callback-hook@1.0.10
|
||||||
check@1.2.5
|
check@1.2.5
|
||||||
ddp@1.3.1
|
ddp@1.4.0
|
||||||
ddp-client@2.1.3
|
ddp-client@2.2.0
|
||||||
ddp-common@1.2.9
|
ddp-common@1.3.0
|
||||||
ddp-rate-limiter@1.0.7
|
ddp-rate-limiter@1.0.7
|
||||||
ddp-server@2.0.2
|
ddp-server@2.1.1
|
||||||
diff-sequence@1.0.7
|
diff-sequence@1.0.7
|
||||||
dynamic-import@0.1.3
|
dynamic-import@0.2.1
|
||||||
ecmascript@0.8.3
|
ecmascript@0.9.0
|
||||||
ecmascript-runtime@0.4.1
|
ecmascript-runtime@0.5.0
|
||||||
ecmascript-runtime-client@0.4.3
|
ecmascript-runtime-client@0.5.0
|
||||||
ecmascript-runtime-server@0.4.1
|
ecmascript-runtime-server@0.5.0
|
||||||
ejson@1.0.14
|
ejson@1.1.0
|
||||||
email@1.2.3
|
email@1.2.3
|
||||||
example-simple@0.0.0
|
example-simple@0.0.0
|
||||||
fourseven:scss@4.5.4
|
fourseven:scss@4.5.4
|
||||||
geojson-utils@1.0.10
|
geojson-utils@1.0.10
|
||||||
hot-code-push@1.0.4
|
hot-code-push@1.0.4
|
||||||
http@1.2.12
|
http@1.3.0
|
||||||
id-map@1.0.9
|
id-map@1.0.9
|
||||||
livedata@1.0.18
|
livedata@1.0.18
|
||||||
localstorage@1.1.1
|
localstorage@1.2.0
|
||||||
logging@1.1.17
|
logging@1.1.19
|
||||||
meteor@1.7.2
|
meteor@1.8.2
|
||||||
meteor-base@1.1.0
|
meteor-base@1.2.0
|
||||||
meteorhacks:inject-initial@1.0.4
|
meteorhacks:inject-initial@1.0.4
|
||||||
meteorhacks:picker@1.0.3
|
meteorhacks:picker@1.0.3
|
||||||
minifier-css@1.2.16
|
minifier-css@1.2.16
|
||||||
minifier-js@2.1.4
|
minifier-js@2.2.2
|
||||||
minimongo@1.3.2
|
minimongo@1.4.3
|
||||||
modules@0.10.0
|
modules@0.11.2
|
||||||
modules-runtime@0.8.0
|
modules-runtime@0.9.1
|
||||||
mongo@1.2.2
|
mongo@1.3.1
|
||||||
mongo-dev-server@1.0.1
|
mongo-dev-server@1.1.0
|
||||||
mongo-id@1.0.6
|
mongo-id@1.0.6
|
||||||
npm-bcrypt@0.9.3
|
npm-bcrypt@0.9.3
|
||||||
npm-mongo@2.2.30
|
npm-mongo@2.2.33
|
||||||
ordered-dict@1.0.9
|
ordered-dict@1.0.9
|
||||||
percolatestudio:synced-cron@1.1.0
|
percolatestudio:synced-cron@1.1.0
|
||||||
promise@0.9.0
|
promise@0.10.1
|
||||||
random@1.0.10
|
random@1.0.10
|
||||||
rate-limit@1.0.8
|
rate-limit@1.0.8
|
||||||
reactive-dict@1.1.9
|
reactive-dict@1.2.0
|
||||||
reactive-var@1.0.11
|
reactive-var@1.0.11
|
||||||
reload@1.1.11
|
reload@1.1.11
|
||||||
retry@1.0.9
|
retry@1.0.9
|
||||||
|
@ -60,23 +60,23 @@ routepolicy@1.0.12
|
||||||
service-configuration@1.0.11
|
service-configuration@1.0.11
|
||||||
session@1.1.7
|
session@1.1.7
|
||||||
sha@1.0.9
|
sha@1.0.9
|
||||||
shell-server@0.2.4
|
shell-server@0.3.1
|
||||||
srp@1.0.10
|
srp@1.0.10
|
||||||
standard-minifier-css@1.3.5
|
standard-minifier-css@1.3.5
|
||||||
standard-minifier-js@2.1.2
|
standard-minifier-js@2.2.3
|
||||||
standard-minifiers@1.1.0
|
standard-minifiers@1.1.0
|
||||||
tracker@1.1.3
|
tracker@1.1.3
|
||||||
underscore@1.0.10
|
underscore@1.0.10
|
||||||
url@1.1.0
|
url@1.1.0
|
||||||
vulcan:accounts@1.8.0
|
vulcan:accounts@1.8.5
|
||||||
vulcan:core@1.8.0
|
vulcan:core@1.8.5
|
||||||
vulcan:debug@1.8.0
|
vulcan:debug@1.8.5
|
||||||
vulcan:email@1.8.0
|
vulcan:email@1.8.5
|
||||||
vulcan:forms@1.8.0
|
vulcan:forms@1.8.5
|
||||||
vulcan:i18n@1.8.0
|
vulcan:i18n@1.8.5
|
||||||
vulcan:i18n-en-us@1.8.0
|
vulcan:i18n-en-us@1.8.5
|
||||||
vulcan:lib@1.8.0
|
vulcan:lib@1.8.5
|
||||||
vulcan:routing@1.8.0
|
vulcan:routing@1.8.5
|
||||||
vulcan:users@1.8.0
|
vulcan:users@1.8.5
|
||||||
webapp@1.3.19
|
webapp@1.4.0
|
||||||
webapp-hashing@1.0.9
|
webapp-hashing@1.0.9
|
||||||
|
|
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}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -23,7 +23,7 @@ function copySync(origin,target){
|
||||||
//Add Definition Colors
|
//Add Definition Colors
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
|
|
||||||
//Vulkan letters
|
//Vulcan letters
|
||||||
console.log(chalk.gray(' ___ ___ '));
|
console.log(chalk.gray(' ___ ___ '));
|
||||||
console.log(chalk.gray(' '+String.fromCharCode(92))+chalk.redBright(String.fromCharCode(92))+chalk.dim.yellow(String.fromCharCode(92))+chalk.gray(String.fromCharCode(92)+' /')+chalk.dim.yellow('/')+chalk.yellowBright('/')+chalk.gray('/'));
|
console.log(chalk.gray(' '+String.fromCharCode(92))+chalk.redBright(String.fromCharCode(92))+chalk.dim.yellow(String.fromCharCode(92))+chalk.gray(String.fromCharCode(92)+' /')+chalk.dim.yellow('/')+chalk.yellowBright('/')+chalk.gray('/'));
|
||||||
console.log(chalk.gray(' '+String.fromCharCode(92))+chalk.redBright(String.fromCharCode(92))+chalk.dim.yellow(String.fromCharCode(92))+chalk.gray(String.fromCharCode(92))+chalk.gray('/')+chalk.dim.yellow('/')+chalk.yellowBright('/')+chalk.gray('/ Vulcan.js'));
|
console.log(chalk.gray(' '+String.fromCharCode(92))+chalk.redBright(String.fromCharCode(92))+chalk.dim.yellow(String.fromCharCode(92))+chalk.gray(String.fromCharCode(92))+chalk.gray('/')+chalk.dim.yellow('/')+chalk.yellowBright('/')+chalk.gray('/ Vulcan.js'));
|
28
README.md
28
README.md
|
@ -1,10 +1,32 @@
|
||||||
<img src="https://d3vv6lp55qjaqc.cloudfront.net/items/2B3C1z2V2y421p2I0P42/vulcan-logo-noborder.png" width="200">
|
<img src="https://d3vv6lp55qjaqc.cloudfront.net/items/2B3C1z2V2y421p2I0P42/vulcan-logo-noborder.png" width="200">
|
||||||
|
|
||||||
# Vulcan (formerly Telescope)
|
# Vulcan
|
||||||
|
|
||||||
[Version 1.8.0](https://github.com/VulcanJS/Vulcan/releases)
|
[Version 1.8.1](https://github.com/VulcanJS/Vulcan/releases)
|
||||||
|
|
||||||
This is the Apollo/GraphQL version of Telescope, now known as [Vulcan](http://vulcanjs.org). [You can find the documentation here](http://docs.vulcanjs.org/).
|
Vulcan is a React+GraphQL framework for Meteor.
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
- [Full video tutorial](https://www.youtube.com/watch?v=aCjR9UrNqVk)
|
||||||
|
|
||||||
|
Install the latest version of Node and NPM. We recommend the usage of [NVM](http://nvm.sh).
|
||||||
|
|
||||||
|
You can then install [Meteor](https://www.meteor.com/install), which is used as the Vulcan build tool.
|
||||||
|
|
||||||
|
Clone the [Vulcan Starter repo](https://github.com/VulcanJS/Vulcan-Starter) locally.
|
||||||
|
|
||||||
|
Rename your `sample_settings.json` file to `settings.json`, then:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
And open `http://localhost:3000/` in your browser.
|
||||||
|
|
||||||
|
|
||||||
|
Find more info in the [documentation](http://docs.vulcanjs.org/#Install).
|
||||||
|
|
||||||
### Links
|
### Links
|
||||||
|
|
||||||
|
|
15
jsconfig.json
Normal file
15
jsconfig.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES6"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"packages/**/*"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"packages/_buffer",
|
||||||
|
"packages/_boilerplate-generator"
|
||||||
|
],
|
||||||
|
"typeAcquisition": {
|
||||||
|
"enable": true
|
||||||
|
},
|
||||||
|
}
|
6333
package-lock.json
generated
6333
package-lock.json
generated
File diff suppressed because it is too large
Load diff
43
package.json
43
package.json
|
@ -1,23 +1,25 @@
|
||||||
{
|
{
|
||||||
"name": "Vulcan",
|
"name": "Vulcan",
|
||||||
"version": "1.8.0",
|
"version": "1.8.5",
|
||||||
"engines": {
|
"engines": {
|
||||||
"npm": "^3.0"
|
"npm": "^3.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prestart": "node prestart_vulcan.js",
|
|
||||||
"start": "meteor --settings settings.json",
|
"start": "meteor --settings settings.json",
|
||||||
"lint": "eslint --cache --ext .jsx,js packages"
|
"lint": "eslint --cache --ext .jsx,js packages"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "2.2.0",
|
|
||||||
"analytics-node": "^2.1.1",
|
"analytics-node": "^2.1.1",
|
||||||
"apollo-client": "^1.2.2",
|
"apollo-client": "^1.2.2",
|
||||||
|
"apollo-engine": "^0.5.4",
|
||||||
"apollo-errors": "^1.4.0",
|
"apollo-errors": "^1.4.0",
|
||||||
"babel-runtime": "^6.18.0",
|
"apollo-server-express": "^1.2.0",
|
||||||
|
"babel-runtime": "^6.26.0",
|
||||||
"bcrypt": "^0.8.7",
|
"bcrypt": "^0.8.7",
|
||||||
"body-parser": "^1.15.2",
|
"body-parser": "^1.18.2",
|
||||||
|
"chalk": "2.2.0",
|
||||||
"classnames": "^2.2.3",
|
"classnames": "^2.2.3",
|
||||||
|
"compression": "^1.7.1",
|
||||||
"cookie-parser": "^1.4.3",
|
"cookie-parser": "^1.4.3",
|
||||||
"cross-fetch": "^0.0.8",
|
"cross-fetch": "^0.0.8",
|
||||||
"crypto-js": "^3.1.9-1",
|
"crypto-js": "^3.1.9-1",
|
||||||
|
@ -27,11 +29,10 @@
|
||||||
"express": "^4.14.0",
|
"express": "^4.14.0",
|
||||||
"flat": "^4.0.0",
|
"flat": "^4.0.0",
|
||||||
"formsy-react": "^0.19.5",
|
"formsy-react": "^0.19.5",
|
||||||
"formsy-react-components": "^0.10.1",
|
"formsy-react-components": "^0.11.1",
|
||||||
"graphql": "^0.9.6",
|
"graphql": "^0.9.6",
|
||||||
"graphql-anywhere": "^3.0.1",
|
"graphql-anywhere": "^3.0.1",
|
||||||
"graphql-date": "^1.0.2",
|
"graphql-date": "^1.0.2",
|
||||||
"graphql-server-express": "^0.6.0",
|
|
||||||
"graphql-tag": "^2.0.0",
|
"graphql-tag": "^2.0.0",
|
||||||
"graphql-tools": "^0.10.1",
|
"graphql-tools": "^0.10.1",
|
||||||
"graphql-type-json": "^0.1.4",
|
"graphql-type-json": "^0.1.4",
|
||||||
|
@ -45,35 +46,35 @@
|
||||||
"intl-locales-supported": "^1.0.0",
|
"intl-locales-supported": "^1.0.0",
|
||||||
"juice": "^1.11.0",
|
"juice": "^1.11.0",
|
||||||
"mailchimp": "^1.1.6",
|
"mailchimp": "^1.1.6",
|
||||||
"marked": "^0.3.5",
|
"marked": "^0.3.9",
|
||||||
"metascraper": "^1.0.6",
|
"metascraper": "^1.0.6",
|
||||||
"meteor-node-stubs": "^0.2.3",
|
"meteor-node-stubs": "^0.2.3",
|
||||||
"mingo": "^0.8.1",
|
"mingo": "^0.8.1",
|
||||||
"moment": "^2.13.0",
|
"moment": "^2.13.0",
|
||||||
"optics-agent": "^1.0.5",
|
"prop-types": "^15.6.0",
|
||||||
"prop-types": "^15.5.10",
|
"react": "^16.2.0",
|
||||||
"react": "^15.6.1",
|
|
||||||
"react-addons-pure-render-mixin": "^15.4.1",
|
"react-addons-pure-render-mixin": "^15.4.1",
|
||||||
"react-apollo": "^1.1.1",
|
"react-apollo": "^1.4.15",
|
||||||
"react-bootstrap": "^0.30.7",
|
"react-bootstrap": "^0.32.0",
|
||||||
"react-bootstrap-datetimepicker": "0.0.22",
|
"react-bootstrap-datetimepicker": "0.0.22",
|
||||||
"react-cookie": "^0.4.6",
|
"react-cookie": "^0.4.6",
|
||||||
"react-datetime": "^2.3.2",
|
"react-datetime": "^2.11.1",
|
||||||
"react-dom": "^15.4.1",
|
"react-dom": "^16.2.0",
|
||||||
"react-dropzone": "^3.12.2",
|
"react-dropzone": "^3.12.2",
|
||||||
"react-helmet": "^5.1.3",
|
"react-helmet": "^5.1.3",
|
||||||
"react-intl": "^2.1.3",
|
"react-intl": "^2.1.3",
|
||||||
"react-loadable": "^4.0.3",
|
"react-loadable": "^4.0.3",
|
||||||
|
"react-markdown": "^3.1.4",
|
||||||
"react-places-autocomplete": "^5.0.0",
|
"react-places-autocomplete": "^5.0.0",
|
||||||
"react-redux": "^5.0.1",
|
"react-redux": "^5.0.6",
|
||||||
"react-router": "^3.0.0",
|
"react-router": "^3.2.0",
|
||||||
"react-router-bootstrap": "^0.23.1",
|
"react-router-bootstrap": "^0.23.1",
|
||||||
"react-router-scroll": "^0.4.1",
|
"react-router-scroll": "^0.4.4",
|
||||||
"react-stripe-checkout": "2.4.0",
|
"react-stripe-checkout": "^2.4.0",
|
||||||
"recompose": "^0.21.2",
|
"recompose": "^0.26.0",
|
||||||
"redux": "^3.6.0",
|
"redux": "^3.6.0",
|
||||||
"rss": "^1.2.1",
|
"rss": "^1.2.1",
|
||||||
"sanitize-html": "^1.11.4",
|
"sanitize-html": "^1.16.3",
|
||||||
"sendy-api": "^0.1.0",
|
"sendy-api": "^0.1.0",
|
||||||
"simpl-schema": "^0.3.2",
|
"simpl-schema": "^0.3.2",
|
||||||
"speakingurl": "^9.0.0",
|
"speakingurl": "^9.0.0",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: "boilerplate-generator",
|
name: "boilerplate-generator",
|
||||||
summary: "Generates the boilerplate html from program's manifest",
|
summary: "Generates the boilerplate html from program's manifest",
|
||||||
version: '1.2.0'
|
version: '1.3.0'
|
||||||
});
|
});
|
||||||
|
|
||||||
Package.onUse(api => {
|
Package.onUse(api => {
|
||||||
|
@ -11,4 +11,4 @@ Package.onUse(api => {
|
||||||
], 'server');
|
], 'server');
|
||||||
api.mainModule('generator.js', 'server');
|
api.mainModule('generator.js', 'server');
|
||||||
api.export('Boilerplate', 'server');
|
api.export('Boilerplate', 'server');
|
||||||
});
|
});
|
|
@ -5,8 +5,8 @@ Package.describe({
|
||||||
Package.onUse( function(api) {
|
Package.onUse( function(api) {
|
||||||
|
|
||||||
api.use([
|
api.use([
|
||||||
'vulcan:core',
|
'vulcan:core@1.8.5',
|
||||||
'example-forum',
|
'example-forum@1.8.5',
|
||||||
|
|
||||||
'fourseven:scss@4.5.0',
|
'fourseven:scss@4.5.0',
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Components, registerComponent, withCurrentUser } from 'meteor/vulcan:core';
|
import { Components, registerComponent, withCurrentUser } from 'meteor/vulcan:core';
|
||||||
import React, { PropTypes, Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Helmet from 'react-helmet';
|
import Helmet from 'react-helmet';
|
||||||
|
|
||||||
|
|
|
@ -17,10 +17,16 @@ class Newsletter extends PureComponent {
|
||||||
this.dismissBanner = this.dismissBanner.bind(this);
|
this.dismissBanner = this.dismissBanner.bind(this);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
showBanner: showBanner(props.currentUser)
|
showBanner: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.setState({
|
||||||
|
showBanner: showBanner(this.props.currentUser)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps, nextContext) {
|
componentWillReceiveProps(nextProps, nextContext) {
|
||||||
if (nextProps.currentUser) {
|
if (nextProps.currentUser) {
|
||||||
this.setState({showBanner: showBanner(nextProps.currentUser)});
|
this.setState({showBanner: showBanner(nextProps.currentUser)});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Components, registerComponent, withMutation, withCurrentUser, withMessages, Utils } from 'meteor/vulcan:core';
|
import { Components, registerComponent, withMutation, withCurrentUser, withMessages, Utils } from 'meteor/vulcan:core';
|
||||||
import React, { PropTypes, Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { FormattedMessage, intlShape } from 'meteor/vulcan:i18n';
|
import { FormattedMessage, intlShape } from 'meteor/vulcan:i18n';
|
||||||
import Button from 'react-bootstrap/lib/Button';
|
import Button from 'react-bootstrap/lib/Button';
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { registerComponent, Components, Utils } from 'meteor/vulcan:core';
|
import { registerComponent, Components, Utils } from 'meteor/vulcan:core';
|
||||||
import React, { PropTypes, Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { intlShape } from 'meteor/vulcan:i18n';
|
import { intlShape } from 'meteor/vulcan:i18n';
|
||||||
import Formsy from 'formsy-react';
|
import Formsy from 'formsy-react';
|
||||||
import FRC from 'formsy-react-components';
|
import FRC from 'formsy-react-components';
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Components, registerComponent, getSetting, registerSetting } from 'meteor/vulcan:core';
|
import { Components, registerComponent, getSetting, registerSetting } from 'meteor/vulcan:core';
|
||||||
import React, { PropTypes, Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
registerSetting('forum.numberOfDays', 5, 'Number of days to display in Daily view');
|
registerSetting('forum.numberOfDays', 5, 'Number of days to display in Daily view');
|
||||||
|
|
|
@ -27,7 +27,6 @@ class PostsEditForm extends PureComponent {
|
||||||
<Components.SmartForm
|
<Components.SmartForm
|
||||||
collection={Posts}
|
collection={Posts}
|
||||||
documentId={this.props.post._id}
|
documentId={this.props.post._id}
|
||||||
mutationFragment={getFragment('PostsPage')}
|
|
||||||
successCallback={post => {
|
successCallback={post => {
|
||||||
this.props.closeModal();
|
this.props.closeModal();
|
||||||
this.props.flash(this.context.intl.formatMessage({ id: 'posts.edit_success' }, { title: post.title }), 'success');
|
this.props.flash(this.context.intl.formatMessage({ id: 'posts.edit_success' }, { title: post.title }), 'success');
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Components, registerComponent } from 'meteor/vulcan:core';
|
import { Components, registerComponent } from 'meteor/vulcan:core';
|
||||||
import React, { PropTypes, Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const PostsHome = (props, context) => {
|
const PostsHome = (props, context) => {
|
||||||
const terms = _.isEmpty(props.location && props.location.query) ? {view: 'top'}: props.location.query;
|
const terms = _.isEmpty(props.location && props.location.query) ? {view: 'top'}: props.location.query;
|
||||||
|
|
|
@ -24,10 +24,10 @@ const PostsNewForm = (props, context) =>
|
||||||
</Components.ShowIf>
|
</Components.ShowIf>
|
||||||
|
|
||||||
PostsNewForm.propTypes = {
|
PostsNewForm.propTypes = {
|
||||||
closeModal: React.PropTypes.func,
|
closeModal: PropTypes.func,
|
||||||
router: React.PropTypes.object,
|
router: PropTypes.object,
|
||||||
flash: React.PropTypes.func,
|
flash: PropTypes.func,
|
||||||
redirect: React.PropTypes.string,
|
redirect: PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
PostsNewForm.contextTypes = {
|
PostsNewForm.contextTypes = {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Components, registerComponent, withDocument, withCurrentUser, getActions, withMutation } from 'meteor/vulcan:core';
|
import { Components, registerComponent, withDocument, withCurrentUser, getActions, withMutation } from 'meteor/vulcan:core';
|
||||||
import { Posts } from '../../modules/posts/index.js';
|
import { Posts } from '../../modules/posts/index.js';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { FormattedMessage } from 'meteor/vulcan:i18n';
|
import { FormattedMessage } from 'meteor/vulcan:i18n';
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Components, registerComponent } from 'meteor/vulcan:core';
|
import { Components, registerComponent } from 'meteor/vulcan:core';
|
||||||
import React, { PropTypes, Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { FormattedMessage } from 'meteor/vulcan:i18n';
|
import { FormattedMessage } from 'meteor/vulcan:i18n';
|
||||||
import Dropdown from 'react-bootstrap/lib/Dropdown';
|
import Dropdown from 'react-bootstrap/lib/Dropdown';
|
||||||
import { STATES } from 'meteor/vulcan:accounts';
|
import { STATES } from 'meteor/vulcan:accounts';
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { getCategoriesAsOptions } from './schema.js';
|
||||||
|
|
||||||
Posts.addField([
|
Posts.addField([
|
||||||
{
|
{
|
||||||
fieldName: 'categories',
|
fieldName: 'categoriesIds',
|
||||||
fieldSchema: {
|
fieldSchema: {
|
||||||
type: Array,
|
type: Array,
|
||||||
control: 'checkboxgroup',
|
control: 'checkboxgroup',
|
||||||
|
@ -17,12 +17,17 @@ Posts.addField([
|
||||||
insertableBy: ['members'],
|
insertableBy: ['members'],
|
||||||
editableBy: ['members'],
|
editableBy: ['members'],
|
||||||
viewableBy: ['guests'],
|
viewableBy: ['guests'],
|
||||||
form: {
|
options: props => {
|
||||||
noselect: true,
|
return getCategoriesAsOptions(props.data.CategoriesList);
|
||||||
type: 'bootstrap-category',
|
|
||||||
order: 50,
|
|
||||||
options: formProps => getCategoriesAsOptions(formProps.client),
|
|
||||||
},
|
},
|
||||||
|
query: `
|
||||||
|
CategoriesList{
|
||||||
|
_id
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
order
|
||||||
|
}
|
||||||
|
`,
|
||||||
resolveAs: {
|
resolveAs: {
|
||||||
fieldName: 'categories',
|
fieldName: 'categories',
|
||||||
type: '[Category]',
|
type: '[Category]',
|
||||||
|
@ -30,12 +35,13 @@ Posts.addField([
|
||||||
if (!post.categories) return [];
|
if (!post.categories) return [];
|
||||||
const categories = _.compact(await Categories.loader.loadMany(post.categories));
|
const categories = _.compact(await Categories.loader.loadMany(post.categories));
|
||||||
return Users.restrictViewableFields(currentUser, Categories, categories);
|
return Users.restrictViewableFields(currentUser, Categories, categories);
|
||||||
}
|
},
|
||||||
|
addOriginalField: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'categories.$',
|
fieldName: 'categoriesIds.$',
|
||||||
fieldSchema: {
|
fieldSchema: {
|
||||||
type: String,
|
type: String,
|
||||||
optional: true
|
optional: true
|
||||||
|
|
|
@ -7,36 +7,18 @@ Categories schema
|
||||||
import { Utils } from 'meteor/vulcan:core';
|
import { Utils } from 'meteor/vulcan:core';
|
||||||
import { Categories } from './collection.js';
|
import { Categories } from './collection.js';
|
||||||
|
|
||||||
export function getCategories (apolloClient) {
|
export function getCategoriesAsOptions (categories) {
|
||||||
|
// give the form component (here: checkboxgroup) exploitable data
|
||||||
// get the current data of the store
|
return categories.map(category => ({
|
||||||
const apolloData = apolloClient.store.getState().apollo.data;
|
value: category._id,
|
||||||
|
label: category.name,
|
||||||
// filter these data based on their typename: we are interested in the categories data
|
// slug: category.slug, // note: it may be used to look up from prefilled props
|
||||||
let categories = _.filter(apolloData, (object, key) => {
|
}));
|
||||||
return object.__typename === 'Category'
|
|
||||||
});
|
|
||||||
|
|
||||||
// order categories
|
|
||||||
categories = _.sortBy(categories, cat => cat.order);
|
|
||||||
|
|
||||||
return categories;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCategoriesAsOptions (apolloClient) {
|
export function getCategoriesAsNestedOptions (categories) {
|
||||||
// give the form component (here: checkboxgroup) exploitable data
|
// give the form component (here: checkboxgroup) exploitable data
|
||||||
return getCategories(apolloClient).map(function (category) {
|
const formattedCategories = categories.map(function (category) {
|
||||||
return {
|
|
||||||
value: category._id,
|
|
||||||
label: category.name,
|
|
||||||
// slug: category.slug, // note: it may be used to look up from prefilled props
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCategoriesAsNestedOptions (apolloClient) {
|
|
||||||
// give the form component (here: checkboxgroup) exploitable data
|
|
||||||
const formattedCategories = getCategories(apolloClient).map(function (category) {
|
|
||||||
return {
|
return {
|
||||||
value: category._id,
|
value: category._id,
|
||||||
label: category.name,
|
label: category.name,
|
||||||
|
@ -122,9 +104,17 @@ const schema = {
|
||||||
},
|
},
|
||||||
addOriginalField: true
|
addOriginalField: true
|
||||||
},
|
},
|
||||||
form: {
|
options: props => {
|
||||||
options: formProps => getCategoriesAsOptions(formProps.client)
|
return getCategoriesAsOptions(props.data.CategoriesList);
|
||||||
}
|
},
|
||||||
|
query: `
|
||||||
|
CategoriesList{
|
||||||
|
_id
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
order
|
||||||
|
}
|
||||||
|
`,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,12 @@ registerFragment(`
|
||||||
...UsersMinimumInfo
|
...UsersMinimumInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
# vulcan:voting
|
# voting
|
||||||
|
currentUserVotes{
|
||||||
|
...VoteFragment
|
||||||
|
}
|
||||||
|
baseScore
|
||||||
|
score
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
|
|
@ -77,7 +77,15 @@ const schema = {
|
||||||
editableBy: ['members'],
|
editableBy: ['members'],
|
||||||
control: 'url',
|
control: 'url',
|
||||||
order: 10,
|
order: 10,
|
||||||
searchable: true
|
searchable: true,
|
||||||
|
form: {
|
||||||
|
query: `
|
||||||
|
SiteData{
|
||||||
|
logoUrl
|
||||||
|
title
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
Title
|
Title
|
||||||
|
@ -198,16 +206,19 @@ const schema = {
|
||||||
insertableBy: ['admins'],
|
insertableBy: ['admins'],
|
||||||
editableBy: ['admins'],
|
editableBy: ['admins'],
|
||||||
control: 'select',
|
control: 'select',
|
||||||
onInsert: document => {
|
onInsert: (document, currentUser) => {
|
||||||
if (document.userId && !document.status) {
|
if (!document.status) {
|
||||||
const user = Users.findOne(document.userId);
|
return Posts.getDefaultStatus(currentUser);
|
||||||
return Posts.getDefaultStatus(user);
|
}
|
||||||
|
},
|
||||||
|
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: {
|
form: {
|
||||||
noselect: true,
|
|
||||||
options: () => Posts.statuses,
|
options: () => Posts.statuses,
|
||||||
group: 'admin'
|
|
||||||
},
|
},
|
||||||
group: formGroups.admin
|
group: formGroups.admin
|
||||||
},
|
},
|
||||||
|
@ -367,8 +378,8 @@ const schema = {
|
||||||
optional: true,
|
optional: true,
|
||||||
resolveAs: {
|
resolveAs: {
|
||||||
type: 'String',
|
type: 'String',
|
||||||
resolver: (booking, args, context) => {
|
resolver: (post, args, context) => {
|
||||||
return moment(booking.endAt).format('dddd, MMMM Do YYYY');
|
return moment(post.postedAt).format('dddd, MMMM Do YYYY');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -403,6 +414,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;
|
export default schema;
|
||||||
|
|
|
@ -9,7 +9,8 @@ import { performVoteServer } from 'meteor/vulcan:voting';
|
||||||
*/
|
*/
|
||||||
function CommentsNewUpvoteOwnComment(comment) {
|
function CommentsNewUpvoteOwnComment(comment) {
|
||||||
var commentAuthor = Users.findOne(comment.userId);
|
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});
|
const existingClickEvent = Events.findOne({name: 'click', 'properties.postId': post._id, 'properties.ip': ip});
|
||||||
|
|
||||||
if(!existingClickEvent) {
|
if(!existingClickEvent) {
|
||||||
Events.log(clickEvent);
|
// Events.log(clickEvent); // Sidebar only: don't log event
|
||||||
return Posts.update(post._id, { $inc: { clickCount: 1 }});
|
return Posts.update(post._id, { $inc: { clickCount: 1 }});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -112,4 +112,14 @@ function PostsClickTracking(post, ip) {
|
||||||
// note: this event is not sent to segment cause we cannot access the current user
|
// 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
|
// 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
|
// 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 })};
|
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({
|
Package.describe({
|
||||||
name: "example-forum",
|
name: "example-forum",
|
||||||
summary: "Vulcan forum package",
|
summary: "Vulcan forum package",
|
||||||
version: '1.8.0',
|
version: '1.8.5',
|
||||||
git: "https://github.com/VulcanJS/Vulcan.git"
|
git: "https://github.com/VulcanJS/Vulcan.git"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -14,16 +14,16 @@ Package.onUse(function (api) {
|
||||||
'fourseven:scss@4.5.0',
|
'fourseven:scss@4.5.0',
|
||||||
|
|
||||||
// vulcan core
|
// vulcan core
|
||||||
'vulcan:core@1.8.0',
|
'vulcan:core@1.8.5',
|
||||||
|
|
||||||
// vulcan packages
|
// vulcan packages
|
||||||
'vulcan:voting@1.8.0',
|
'vulcan:voting@1.8.5',
|
||||||
'vulcan:accounts@1.8.0',
|
'vulcan:accounts@1.8.5',
|
||||||
'vulcan:email',
|
'vulcan:email@1.8.5',
|
||||||
'vulcan:forms',
|
'vulcan:forms@1.8.5',
|
||||||
'vulcan:newsletter',
|
'vulcan:newsletter@1.8.5',
|
||||||
'vulcan:events',
|
'vulcan:events@1.8.5',
|
||||||
'vulcan:embed',
|
'vulcan:embed@1.8.5',
|
||||||
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ component (if the "component" prop is specified).
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { registerComponent, Components, withCurrentUser } from 'meteor/vulcan:core';
|
import { registerComponent, Components, withCurrentUser } from 'meteor/vulcan:core';
|
||||||
import Users from 'meteor/vulcan:users';
|
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
|
// navigation bar component when the user is logged in
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ const NavLoggedIn = ({currentUser}) =>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Components.ModalTrigger label="Upload">
|
<Components.ModalTrigger label="Upload">
|
||||||
<PicsNewForm />
|
<Components.PicsNewForm />
|
||||||
</Components.ModalTrigger>
|
</Components.ModalTrigger>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -71,4 +71,4 @@ const Header = ({currentUser}) =>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
registerComponent('Header', Header, withCurrentUser);
|
registerComponent('Header', Header, withCurrentUser);
|
||||||
|
|
|
@ -7,12 +7,12 @@ Package.onUse(function (api) {
|
||||||
api.use([
|
api.use([
|
||||||
|
|
||||||
// vulcan core
|
// vulcan core
|
||||||
'vulcan:core',
|
'vulcan:core@1.8.5',
|
||||||
|
|
||||||
// vulcan packages
|
// vulcan packages
|
||||||
'vulcan:forms',
|
'vulcan:forms@1.8.5',
|
||||||
'vulcan:accounts',
|
'vulcan:accounts@1.8.5',
|
||||||
'vulcan:forms-upload',
|
'vulcan:forms-upload@1.8.5',
|
||||||
|
|
||||||
// third-party packages
|
// third-party packages
|
||||||
'fourseven:scss@4.5.0',
|
'fourseven:scss@4.5.0',
|
||||||
|
|
|
@ -7,11 +7,11 @@ Package.onUse(function (api) {
|
||||||
api.use([
|
api.use([
|
||||||
|
|
||||||
// vulcan core
|
// vulcan core
|
||||||
'vulcan:core',
|
'vulcan:core@1.8.5',
|
||||||
|
|
||||||
// vulcan packages
|
// vulcan packages
|
||||||
'vulcan:forms',
|
'vulcan:forms@1.8.5',
|
||||||
'vulcan:accounts',
|
'vulcan:accounts@1.8.5',
|
||||||
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ component (if the "component" prop is specified).
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { registerComponent, Components, withCurrentUser } from 'meteor/vulcan:core';
|
import { registerComponent, Components, withCurrentUser } from 'meteor/vulcan:core';
|
||||||
import Users from 'meteor/vulcan:users';
|
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
|
// navigation bar component when the user is logged in
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ const NavLoggedIn = ({currentUser}) =>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Components.ModalTrigger label="Upload">
|
<Components.ModalTrigger label="Upload">
|
||||||
<PicsNewForm />
|
<Components.PicsNewForm />
|
||||||
</Components.ModalTrigger>
|
</Components.ModalTrigger>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -71,4 +71,4 @@ const Header = ({currentUser}) =>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
registerComponent('Header', Header, withCurrentUser);
|
registerComponent('Header', Header, withCurrentUser);
|
||||||
|
|
|
@ -7,13 +7,13 @@ Package.onUse(function (api) {
|
||||||
api.use([
|
api.use([
|
||||||
|
|
||||||
// vulcan core
|
// vulcan core
|
||||||
'vulcan:core',
|
'vulcan:core@1.8.5',
|
||||||
|
|
||||||
// vulcan packages
|
// vulcan packages
|
||||||
'vulcan:forms',
|
'vulcan:forms@1.8.5',
|
||||||
'vulcan:accounts',
|
'vulcan:accounts@1.8.5',
|
||||||
'vulcan:forms-upload',
|
'vulcan:forms-upload@1.8.5',
|
||||||
'vulcan:payments',
|
'vulcan:payments@1.8.5',
|
||||||
|
|
||||||
// third-party packages
|
// third-party packages
|
||||||
'fourseven:scss@4.5.0',
|
'fourseven:scss@4.5.0',
|
||||||
|
|
|
@ -7,11 +7,11 @@ Package.onUse(function (api) {
|
||||||
api.use([
|
api.use([
|
||||||
|
|
||||||
// vulcan core
|
// vulcan core
|
||||||
'vulcan:core',
|
'vulcan:core@1.8.5',
|
||||||
|
|
||||||
// vulcan packages
|
// vulcan packages
|
||||||
'vulcan:forms',
|
'vulcan:forms@1.8.5',
|
||||||
'vulcan:accounts',
|
'vulcan:accounts@1.8.5',
|
||||||
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ component (if the "component" prop is specified).
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { registerComponent, Components, withCurrentUser } from 'meteor/vulcan:core';
|
import { registerComponent, Components, withCurrentUser } from 'meteor/vulcan:core';
|
||||||
import Users from 'meteor/vulcan:users';
|
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
|
// navigation bar component when the user is logged in
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ const NavLoggedIn = ({currentUser}) =>
|
||||||
|
|
||||||
{Users.canDo(currentUser, 'pics.new') ?
|
{Users.canDo(currentUser, 'pics.new') ?
|
||||||
<Components.ModalTrigger label="Upload">
|
<Components.ModalTrigger label="Upload">
|
||||||
<PicsNewForm />
|
<Components.PicsNewForm />
|
||||||
</Components.ModalTrigger>
|
</Components.ModalTrigger>
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,12 +7,12 @@ Package.onUse(function (api) {
|
||||||
api.use([
|
api.use([
|
||||||
|
|
||||||
// vulcan core
|
// vulcan core
|
||||||
'vulcan:core',
|
'vulcan:core@1.8.5',
|
||||||
|
|
||||||
// vulcan packages
|
// vulcan packages
|
||||||
'vulcan:forms',
|
'vulcan:forms@1.8.5',
|
||||||
'vulcan:accounts',
|
'vulcan:accounts@1.8.5',
|
||||||
'vulcan:forms-upload',
|
'vulcan:forms-upload@1.8.5',
|
||||||
|
|
||||||
// third-party packages
|
// third-party packages
|
||||||
'fourseven:scss@4.5.0',
|
'fourseven:scss@4.5.0',
|
||||||
|
|
|
@ -7,12 +7,12 @@ Package.onUse(function (api) {
|
||||||
api.use([
|
api.use([
|
||||||
|
|
||||||
// vulcan core
|
// vulcan core
|
||||||
'vulcan:core',
|
'vulcan:core@1.8.5',
|
||||||
|
|
||||||
// vulcan packages
|
// vulcan packages
|
||||||
'vulcan:voting',
|
'vulcan:voting@1.8.5',
|
||||||
'vulcan:forms',
|
'vulcan:forms@1.8.5',
|
||||||
'vulcan:accounts',
|
'vulcan:accounts@1.8.5',
|
||||||
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
|
// client/main.js #tutorial-step-3 - This is the file that is called into package.js.
|
||||||
import '../modules/index.js';
|
import '../modules/index.js';
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
/*
|
/*
|
||||||
|
|
||||||
List of movies.
|
components/MoviesList.jsx #tutorial-step-7 -
|
||||||
|
The component for our list of movies, which we called in to modules/routes.js.
|
||||||
|
|
||||||
Wrapped with the "withList" and "withCurrentUser" containers.
|
Wrapped with the "withList" and "withCurrentUser" containers.
|
||||||
|
|
||||||
|
#tutorial-step-11 -
|
||||||
|
Now, we are going to look at this in a bit more detail.
|
||||||
|
This component is a React component. We only have one but it does a bunch of things...
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
@ -11,57 +16,109 @@ import { Components, withList, withCurrentUser, registerComponent } from 'meteor
|
||||||
|
|
||||||
import Movies from '../../modules/movies/collection.js';
|
import Movies from '../../modules/movies/collection.js';
|
||||||
|
|
||||||
const MoviesList = ({results = [], currentUser, loading, loadMore, count, totalCount}) =>
|
{
|
||||||
|
/* These are "props". They are variables for the component that are passed by the components parent.
|
||||||
<div style={{maxWidth: '500px', margin: '20px auto'}}>
|
In this case, to create the parent we wrapped the component in "Higer Order Compoents" (See the Higer Order Compoents section below.)
|
||||||
|
By doing this, we can pass on those props to the children of he HOCs and give them access to the props... */
|
||||||
|
}
|
||||||
|
const MoviesList = ({
|
||||||
|
results = [],
|
||||||
|
currentUser,
|
||||||
|
loading,
|
||||||
|
loadMore,
|
||||||
|
count,
|
||||||
|
totalCount
|
||||||
|
}) => (
|
||||||
|
<div style={{ maxWidth: "500px", margin: "20px auto" }}>
|
||||||
|
{/* First, this is a Helment tag. It's a React package that loads head tags. We're using it to load the Bootstrap stylesheet.
|
||||||
|
This is not Vulcan specific but it is an easy way to add tags to the head... */}
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<link name="bootstrap" rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.5/css/bootstrap.min.css"/>
|
<link
|
||||||
|
name="bootstrap"
|
||||||
|
rel="stylesheet"
|
||||||
|
type="text/css"
|
||||||
|
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.5/css/bootstrap.min.css"
|
||||||
|
/>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
|
|
||||||
{/* user accounts */}
|
{/* user accounts */}
|
||||||
|
|
||||||
<div style={{padding: '20px 0', marginBottom: '20px', borderBottom: '1px solid #ccc'}}>
|
<div
|
||||||
|
style={{
|
||||||
|
padding: "20px 0",
|
||||||
|
marginBottom: "20px",
|
||||||
|
borderBottom: "1px solid #ccc"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* ...This is the log in form component. It allowed you to create an account in our web app.
|
||||||
|
It takes care of all accounts stuff like changing passwords, signing in and out, and so on
|
||||||
|
Just pop this in anywhere you want to use it. It's in the Vulcan accounts package... */}
|
||||||
<Components.AccountsLoginForm />
|
<Components.AccountsLoginForm />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ?
|
{/* ... We have a test for the loding variable (See the "Higher Order Components" section at the bottom and then the "props" section at the top.)... */}
|
||||||
|
{loading ? (
|
||||||
<Components.Loading /> :
|
<Components.Loading />
|
||||||
|
) : (
|
||||||
<div className="movies">
|
<div className="movies">
|
||||||
|
{/* new document form - this if for inserting new documents. Because the collection has the schema, when we generate the form, it know what the colleciton should look like
|
||||||
{/* new document form */}
|
You only need to specify the colleciton. You don't need to code any of the form. Validation will work and it will show you fields based on your user permission...*/}
|
||||||
|
|
||||||
{Movies.options.mutations.new.check(currentUser) ?
|
{Movies.options.mutations.new.check(currentUser) ? (
|
||||||
<div style={{marginBottom: '20px', paddingBottom: '20px', borderBottom: '1px solid #ccc'}}>
|
<div
|
||||||
|
style={{
|
||||||
|
marginBottom: "20px",
|
||||||
|
paddingBottom: "20px",
|
||||||
|
borderBottom: "1px solid #ccc"
|
||||||
|
}}
|
||||||
|
>
|
||||||
<h4>Insert New Document</h4>
|
<h4>Insert New Document</h4>
|
||||||
<Components.SmartForm collection={Movies} />
|
<Components.SmartForm collection={Movies} />
|
||||||
</div> :
|
</div>
|
||||||
null
|
) : null}
|
||||||
}
|
|
||||||
|
|
||||||
{/* documents list */}
|
{/* documents list - this is another small utility in Vulcan and it will display it as a card... */}
|
||||||
|
|
||||||
{results.map(movie => <Components.Card fields={['name', 'year', 'review']} key={movie._id} collection={Movies} document={movie} currentUser={currentUser} />)}
|
{results.map(movie => (
|
||||||
|
<Components.Card
|
||||||
{/* load more */}
|
fields={["name", "year", "review"]}
|
||||||
|
key={movie._id}
|
||||||
|
collection={Movies}
|
||||||
|
document={movie}
|
||||||
|
currentUser={currentUser}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
{totalCount > results.length ?
|
{/* load more - this is the load more button. On click we trigger the loadMore function which is passed as a prop by withList... */}
|
||||||
<a href="#" onClick={e => {e.preventDefault(); loadMore();}}>Load More ({count}/{totalCount})</a> :
|
|
||||||
|
{totalCount > results.length ? (
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
loadMore();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Load More ({count}/{totalCount})
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
<p>No more items.</p>
|
<p>No more items.</p>
|
||||||
}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// ...this is where we specify how to load the data in the options object that we pass to withList
|
||||||
|
// if you want, you can specify many more options here, like how often to look for data or what fields to query from, filtering and sorting options. ...
|
||||||
const options = {
|
const options = {
|
||||||
collection: Movies,
|
collection: Movies,
|
||||||
limit: 5,
|
limit: 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// These two functions (withList & withCurrentUser) are Higher Order Components (HOC) and by wrapping our component with this we can give it "props". (See the "props" section at the top.)
|
||||||
registerComponent('MoviesList', MoviesList, withCurrentUser, [withList, options]);
|
registerComponent('MoviesList', MoviesList, withCurrentUser, [withList, options]);
|
||||||
|
|
||||||
|
|
||||||
|
// #tutorial-step-12 - Well. that's it! If you like this, go on to the movies-example, where we get more granular when it comes to data loading.
|
||||||
|
// Well thanks for tuning in! Come visit us at our chat room at slack.vulcanjs.org. See you soon!
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
// modules/index.js - #tutorial-step-8 -
|
||||||
|
// This is where we import our collections
|
||||||
|
|
||||||
// The Movies collection
|
// The Movies collection
|
||||||
import './movies/collection.js';
|
import './movies/collection.js';
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
/*
|
/*
|
||||||
|
|
||||||
The main Movies collection definition file.
|
modules/movies/collection.js - #tutorial-step-9 -
|
||||||
|
This is the main Movies collection definition file.
|
||||||
|
|
||||||
|
A collection in VulcanJS is basically is a model, a type of data, like posts, comments, or users.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -12,17 +15,30 @@ import schema from './schema.js';
|
||||||
|
|
||||||
Movies collection definition
|
Movies collection definition
|
||||||
|
|
||||||
|
We create a new collection with the createCollection function
|
||||||
*/
|
*/
|
||||||
const Movies = createCollection({
|
const Movies = createCollection({
|
||||||
|
|
||||||
|
// It takes the collection name...
|
||||||
collectionName: 'Movies',
|
collectionName: 'Movies',
|
||||||
|
|
||||||
|
// ...the type name, which is it's name of that type's singular instance
|
||||||
|
// usually it is the same as the collection name but singular.
|
||||||
|
// It comes in useful when it is time to build our GraphQL schema...
|
||||||
typeName: 'Movie',
|
typeName: 'Movie',
|
||||||
|
|
||||||
|
// ...this is a JS schema, not GraphQL...
|
||||||
schema,
|
schema,
|
||||||
|
|
||||||
|
// ...then our default resolvers and default mutations...
|
||||||
|
|
||||||
|
// A resolver is the thing that gives you data, that fetches it in the database and sends it to the client.
|
||||||
|
// There are three default resolvers: list - for a list of documents, single - for a single document, and total - for the total number of documents that match a given query.
|
||||||
|
// You can code your own too. Check out the next example, the movies example to do so...
|
||||||
resolvers: getDefaultResolvers('Movies'),
|
resolvers: getDefaultResolvers('Movies'),
|
||||||
|
|
||||||
|
// A mutation is the act of changing data on the server.
|
||||||
|
// There are three default mutaitons: inserting a new document, editing an existing document, and removing a document. You can only do this if you own it.
|
||||||
mutations: getDefaultMutations('Movies'),
|
mutations: getDefaultMutations('Movies'),
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -31,8 +47,11 @@ const Movies = createCollection({
|
||||||
|
|
||||||
Permissions for members (regular users)
|
Permissions for members (regular users)
|
||||||
|
|
||||||
|
...members are default users in Vulcan...
|
||||||
|
|
||||||
*/
|
*/
|
||||||
const membersActions = [
|
const membersActions = [
|
||||||
|
// ...these are the actions that members can do...
|
||||||
'movies.new',
|
'movies.new',
|
||||||
'movies.edit.own',
|
'movies.edit.own',
|
||||||
'movies.remove.own',
|
'movies.remove.own',
|
||||||
|
@ -43,13 +62,22 @@ Users.groups.members.can(membersActions);
|
||||||
|
|
||||||
Default sort
|
Default sort
|
||||||
|
|
||||||
|
This is the default sort view for this data type...
|
||||||
|
|
||||||
*/
|
*/
|
||||||
Movies.addDefaultView(terms => ({
|
Movies.addDefaultView(terms => ({
|
||||||
options: {
|
options: {
|
||||||
sort: {
|
sort: {
|
||||||
|
// ...We want to order by when it was created.
|
||||||
|
// This gets passed to MongoDB.
|
||||||
|
// This will insert in the same order on the server and the client,
|
||||||
|
// which is how the app knew where to put our new Jaws 14 entry in the page.
|
||||||
createdAt: -1
|
createdAt: -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export default Movies;
|
export default Movies;
|
||||||
|
|
||||||
|
// There were three things I mentioned that you might not have heard of:
|
||||||
|
// schema, revolvers, and mutations. I will talk about them in the next steps.
|
||||||
|
|
|
@ -1,22 +1,24 @@
|
||||||
/*
|
/*
|
||||||
|
|
||||||
|
modules/movies/schema.js - #tutorial-step-10 -
|
||||||
|
This is a JS object that defines every property of a collection document...
|
||||||
|
|
||||||
A SimpleSchema-compatible JSON schema
|
A SimpleSchema-compatible JSON schema
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
|
|
||||||
// default properties
|
// default properties
|
||||||
|
|
||||||
_id: {
|
_id: {
|
||||||
type: String,
|
type: String,
|
||||||
optional: true,
|
optional: true,
|
||||||
viewableBy: ['guests'],
|
viewableBy: ["guests"]
|
||||||
},
|
},
|
||||||
createdAt: {
|
createdAt: {
|
||||||
type: Date,
|
type: Date,
|
||||||
optional: true,
|
optional: true,
|
||||||
viewableBy: ['guests'],
|
viewableBy: ["guests"],
|
||||||
onInsert: (document, currentUser) => {
|
onInsert: (document, currentUser) => {
|
||||||
return new Date();
|
return new Date();
|
||||||
}
|
}
|
||||||
|
@ -24,45 +26,50 @@ const schema = {
|
||||||
userId: {
|
userId: {
|
||||||
type: String,
|
type: String,
|
||||||
optional: true,
|
optional: true,
|
||||||
viewableBy: ['guests'],
|
viewableBy: ["guests"],
|
||||||
resolveAs: {
|
resolveAs: {
|
||||||
fieldName: 'user',
|
fieldName: "user",
|
||||||
type: 'User',
|
type: "User",
|
||||||
resolver: (movie, args, context) => {
|
resolver: (movie, args, context) => {
|
||||||
return context.Users.findOne({ _id: movie.userId }, { fields: context.Users.getViewableFields(context.currentUser, context.Users) });
|
return context.Users.findOne(
|
||||||
|
{ _id: movie.userId },
|
||||||
|
{ fields: context.Users.getViewableFields(context.currentUser, context.Users) }
|
||||||
|
);
|
||||||
},
|
},
|
||||||
addOriginalField: true
|
addOriginalField: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// custom properties
|
// custom properties
|
||||||
|
|
||||||
name: {
|
name: {
|
||||||
label: 'Name',
|
label: "Name",
|
||||||
type: String,
|
type: String,
|
||||||
optional: true,
|
optional: true,
|
||||||
viewableBy: ['guests'],
|
// ...these next three are interesting—they take a user group that says which group can do what action.
|
||||||
insertableBy: ['members'],
|
// ...guests are anonymous users...
|
||||||
editableBy: ['members'],
|
viewableBy: ["guests"],
|
||||||
|
/// ...members can only edit documents that they own. This is part of the default mutations. Back to modules/movies/collection.js...
|
||||||
|
insertableBy: ["members"],
|
||||||
|
editableBy: ["members"]
|
||||||
},
|
},
|
||||||
year: {
|
year: {
|
||||||
label: 'Year',
|
label: "Year",
|
||||||
type: String,
|
type: String,
|
||||||
optional: true,
|
optional: true,
|
||||||
viewableBy: ['guests'],
|
viewableBy: ["guests"],
|
||||||
insertableBy: ['members'],
|
insertableBy: ["members"],
|
||||||
editableBy: ['members'],
|
editableBy: ["members"]
|
||||||
},
|
},
|
||||||
review: {
|
review: {
|
||||||
label: 'Review',
|
label: "Review",
|
||||||
type: String,
|
type: String,
|
||||||
optional: true,
|
optional: true,
|
||||||
control: 'textarea',
|
control: "textarea",
|
||||||
viewableBy: ['guests'],
|
viewableBy: ["guests"],
|
||||||
insertableBy: ['members'],
|
insertableBy: ["members"],
|
||||||
editableBy: ['members']
|
editableBy: ["members"]
|
||||||
},
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default schema;
|
export default schema;
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
|
// modules/routes.js #tutorial-step-6 -
|
||||||
|
// Thi is the file that is called into package.js that allows the component to be found.
|
||||||
|
|
||||||
|
// First, we import this from vulcan core, which is a utility to add a new route.
|
||||||
import { addRoute, Components } from 'meteor/vulcan:core';
|
import { addRoute, Components } from 'meteor/vulcan:core';
|
||||||
|
|
||||||
|
// Then, we add the component for what we want to add.
|
||||||
import '../components/movies/MoviesList.jsx';
|
import '../components/movies/MoviesList.jsx';
|
||||||
|
|
||||||
|
// Next, we add the name 'movies', the path, which is the root, and the component name 'MovieList'.
|
||||||
addRoute({ name: 'movies', path: '/', componentName: 'MoviesList' });
|
addRoute({ name: 'movies', path: '/', componentName: 'MoviesList' });
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
|
// server/main.js #tutorial-step-4 - This is the file that is called into package.js.
|
||||||
import '../modules/index.js';
|
import '../modules/index.js';
|
||||||
import './seed.js';
|
import './seed.js';
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
|
|
||||||
Seed the database with some dummy content.
|
server/seed.js #tutorial-step-5 -
|
||||||
|
This is a file to seed the database with some dummy content.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -84,7 +85,6 @@ Meteor.startup(function () {
|
||||||
console.log('// creating dummy movies');
|
console.log('// creating dummy movies');
|
||||||
seedData.forEach(document => {
|
seedData.forEach(document => {
|
||||||
newMutation({
|
newMutation({
|
||||||
action: 'movies.new',
|
|
||||||
collection: Movies,
|
collection: Movies,
|
||||||
document: document,
|
document: document,
|
||||||
currentUser: currentUser,
|
currentUser: currentUser,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// packages.js #tutorial-step-2 - Decribes the contents of the package as well as the dependencies.
|
||||||
|
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'example-simple',
|
name: 'example-simple',
|
||||||
});
|
});
|
||||||
|
@ -5,18 +7,21 @@ Package.describe({
|
||||||
Package.onUse(function (api) {
|
Package.onUse(function (api) {
|
||||||
|
|
||||||
api.use([
|
api.use([
|
||||||
|
|
||||||
|
// Here are our dependencies:
|
||||||
|
|
||||||
// vulcan core
|
// vulcan core
|
||||||
'vulcan:core',
|
'vulcan:core@1.8.5',
|
||||||
|
|
||||||
// vulcan packages
|
// vulcan packages
|
||||||
'vulcan:forms',
|
'vulcan:forms@1.8.5',
|
||||||
'vulcan:accounts',
|
'vulcan:accounts@1.8.5',
|
||||||
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
api.addFiles('lib/stylesheets/style.css');
|
api.addFiles('lib/stylesheets/style.css');
|
||||||
|
|
||||||
|
// Here is the entry point for client & server:
|
||||||
api.mainModule('lib/server/main.js', 'server');
|
api.mainModule('lib/server/main.js', 'server');
|
||||||
api.mainModule('lib/client/main.js', 'client');
|
api.mainModule('lib/client/main.js', 'client');
|
||||||
|
|
||||||
|
|
|
@ -21,14 +21,14 @@ Accounts.ui._options = {
|
||||||
passwordSignupFields: 'USERNAME_AND_EMAIL',
|
passwordSignupFields: 'USERNAME_AND_EMAIL',
|
||||||
minimumPasswordLength: 7,
|
minimumPasswordLength: 7,
|
||||||
loginPath: '/',
|
loginPath: '/',
|
||||||
signUpPath: null,
|
signUpPath: '/',
|
||||||
resetPasswordPath: null,
|
resetPasswordPath: null,
|
||||||
profilePath: '/',
|
profilePath: '/',
|
||||||
changePasswordPath: null,
|
changePasswordPath: null,
|
||||||
homeRoutePath: '/',
|
homeRoutePath: '/',
|
||||||
onSubmitHook: () => {},
|
onSubmitHook: () => {},
|
||||||
onPreSignUpHook: () => new Promise(resolve => resolve()),
|
onPreSignUpHook: () => new Promise(resolve => resolve()),
|
||||||
onPostSignUpHook: () => {},
|
onPostSignUpHook: () => redirect(`${Accounts.ui._options.signUpPath}`),
|
||||||
onEnrollAccountHook: () => redirect(`${Accounts.ui._options.loginPath}`),
|
onEnrollAccountHook: () => redirect(`${Accounts.ui._options.loginPath}`),
|
||||||
onResetPasswordHook: () => redirect(`${Accounts.ui._options.loginPath}`),
|
onResetPasswordHook: () => redirect(`${Accounts.ui._options.loginPath}`),
|
||||||
onVerifyEmailHook: () => redirect(`${Accounts.ui._options.profilePath}`),
|
onVerifyEmailHook: () => redirect(`${Accounts.ui._options.profilePath}`),
|
||||||
|
|
|
@ -3,6 +3,13 @@ import PropTypes from 'prop-types';
|
||||||
import FormControl from 'react-bootstrap/lib/FormControl';
|
import FormControl from 'react-bootstrap/lib/FormControl';
|
||||||
import { registerComponent } from 'meteor/vulcan:core';
|
import { registerComponent } from 'meteor/vulcan:core';
|
||||||
|
|
||||||
|
const autocompleteValues = {
|
||||||
|
'username': 'username',
|
||||||
|
'usernameOrEmail': 'email',
|
||||||
|
'email': 'email',
|
||||||
|
'password': 'current-password'
|
||||||
|
}
|
||||||
|
|
||||||
export class AccountsField extends PureComponent {
|
export class AccountsField extends PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -51,9 +58,12 @@ export class AccountsField extends PureComponent {
|
||||||
if (type == 'notice') {
|
if (type == 'notice') {
|
||||||
return <div className={ className }>{ label }</div>;
|
return <div className={ className }>{ label }</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const autoComplete = autocompleteValues[id];
|
||||||
|
|
||||||
return mount ? (
|
return mount ? (
|
||||||
<div className={ className } style={{marginBottom: '10px'}}>
|
<div className={ className } style={{marginBottom: '10px'}}>
|
||||||
<FormControl id={ id } type={ type } inputRef={ref => { this.input = ref; }} onChange={ onChange } placeholder={ hint } defaultValue={ defaultValue } />
|
<FormControl id={ id } type={ type } inputRef={ref => { this.input = ref; }} onChange={ onChange } placeholder={ hint } defaultValue={ defaultValue } autoComplete={autoComplete }/>
|
||||||
{message && (
|
{message && (
|
||||||
<span className={['message', message.type].join(' ').trim()}>
|
<span className={['message', message.type].join(' ').trim()}>
|
||||||
{message.message}</span>
|
{message.message}</span>
|
||||||
|
|
40
packages/vulcan-accounts/imports/ui/components/LoginForm.jsx
Executable file → Normal file
40
packages/vulcan-accounts/imports/ui/components/LoginForm.jsx
Executable file → Normal file
|
@ -49,16 +49,28 @@ export class AccountsLoginForm extends Tracker.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const doNothing = () => {};
|
||||||
|
|
||||||
|
const defaultHooks = {
|
||||||
|
onPreSignUpHook: props.redirect ? Accounts.ui._options.onPreSignUpHook : doNothing,
|
||||||
|
onPostSignUpHook: props.redirect ? Accounts.ui._options.onPostSignUpHook : doNothing,
|
||||||
|
onEnrollAccountHook: props.redirect ? Accounts.ui._options.onEnrollAccountHook : doNothing,
|
||||||
|
onResetPasswordHook: props.redirect ? Accounts.ui._options.onResetPasswordHook : doNothing,
|
||||||
|
onVerifyEmailHook: props.redirect ? Accounts.ui._options.onVerifyEmailHook : doNothing,
|
||||||
|
onSignedInHook: props.redirect ? Accounts.ui._options.onSignedInHook : doNothing,
|
||||||
|
onSignedOutHook: props.redirect ? Accounts.ui._options.onSignedOutHook : doNothing,
|
||||||
|
};
|
||||||
|
|
||||||
// Set inital state.
|
// Set inital state.
|
||||||
this.state = {
|
this.state = {
|
||||||
messages: [],
|
messages: [],
|
||||||
waiting: true,
|
waiting: false,
|
||||||
formState: props.formState ? props.formState : (currentUser ? STATES.PROFILE : STATES.SIGN_IN),
|
formState: props.formState ? props.formState : (currentUser ? STATES.PROFILE : STATES.SIGN_IN),
|
||||||
onSubmitHook: props.onSubmitHook || Accounts.ui._options.onSubmitHook,
|
onSubmitHook: props.onSubmitHook || Accounts.ui._options.onSubmitHook,
|
||||||
onSignedInHook: resetStoreAndThen(postLogInAndThen(props.onSignedInHook || Accounts.ui._options.onSignedInHook)),
|
onSignedInHook: resetStoreAndThen(postLogInAndThen(props.onSignedInHook || defaultHooks.onSignedInHook)),
|
||||||
onSignedOutHook: resetStoreAndThen(props.onSignedOutHook || Accounts.ui._options.onSignedOutHook),
|
onSignedOutHook: resetStoreAndThen(props.onSignedOutHook || defaultHooks.onSignedOutHook),
|
||||||
onPreSignUpHook: props.onPreSignUpHook || Accounts.ui._options.onPreSignUpHook,
|
onPreSignUpHook: props.onPreSignUpHook || defaultHooks.onPreSignUpHook,
|
||||||
onPostSignUpHook: resetStoreAndThen(postLogInAndThen(props.onPostSignUpHook || Accounts.ui._options.onPostSignUpHook)),
|
onPostSignUpHook: resetStoreAndThen(postLogInAndThen(props.onPostSignUpHook || defaultHooks.onPostSignUpHook)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -647,7 +659,7 @@ export class AccountsLoginForm extends Tracker.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
loginResultCallback(() => this.state.onSignedInHook());
|
loginResultCallback(() => this.state.onSignedInHook(this.props));
|
||||||
this.setState({
|
this.setState({
|
||||||
formState: STATES.PROFILE,
|
formState: STATES.PROFILE,
|
||||||
password: null,
|
password: null,
|
||||||
|
@ -718,7 +730,7 @@ export class AccountsLoginForm extends Tracker.Component {
|
||||||
this.setState({ formState: STATES.PROFILE });
|
this.setState({ formState: STATES.PROFILE });
|
||||||
this.clearDefaultFieldValues();
|
this.clearDefaultFieldValues();
|
||||||
loginResultCallback(() => {
|
loginResultCallback(() => {
|
||||||
Meteor.setTimeout(() => this.state.onSignedInHook(), 100);
|
Meteor.setTimeout(() => this.state.onSignedInHook(this.props), 100);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -733,7 +745,10 @@ export class AccountsLoginForm extends Tracker.Component {
|
||||||
password,
|
password,
|
||||||
formState,
|
formState,
|
||||||
onSubmitHook
|
onSubmitHook
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
|
const self = this;
|
||||||
|
|
||||||
let error = false;
|
let error = false;
|
||||||
this.clearMessages();
|
this.clearMessages();
|
||||||
|
|
||||||
|
@ -792,13 +807,13 @@ export class AccountsLoginForm extends Tracker.Component {
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
onSubmitHook(null, formState);
|
onSubmitHook(null, formState);
|
||||||
this.setState({ formState: STATES.PROFILE, password: null });
|
self.setState({ formState: STATES.PROFILE, password: null });
|
||||||
let currentUser = Accounts.user();
|
let currentUser = Accounts.user();
|
||||||
loginResultCallback(this.state.onPostSignUpHook.bind(this, _options, currentUser));
|
loginResultCallback(self.state.onPostSignUpHook.bind(self, _options, currentUser));
|
||||||
this.clearDefaultFieldValues();
|
self.clearDefaultFieldValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ waiting: false });
|
self.setState({ waiting: false });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
if (!error) {
|
if (!error) {
|
||||||
|
@ -973,6 +988,7 @@ AccountsLoginForm.propTypes = {
|
||||||
AccountsLoginForm.defaultProps = {
|
AccountsLoginForm.defaultProps = {
|
||||||
showSignInLink: true,
|
showSignInLink: true,
|
||||||
showSignUpLink: true,
|
showSignUpLink: true,
|
||||||
|
redirect: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
AccountsLoginForm.contextTypes = {
|
AccountsLoginForm.contextTypes = {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'vulcan:accounts',
|
name: 'vulcan:accounts',
|
||||||
version: '1.8.0',
|
version: '1.8.5',
|
||||||
summary: 'Accounts UI for React in Meteor 1.3+',
|
summary: 'Accounts UI for React in Meteor 1.3+',
|
||||||
git: 'https://github.com/studiointeract/accounts-ui',
|
git: 'https://github.com/studiointeract/accounts-ui',
|
||||||
documentation: 'README.md'
|
documentation: 'README.md'
|
||||||
|
@ -9,7 +9,7 @@ Package.describe({
|
||||||
Package.onUse(function(api) {
|
Package.onUse(function(api) {
|
||||||
api.versionsFrom('1.3');
|
api.versionsFrom('1.3');
|
||||||
|
|
||||||
api.use('vulcan:core@1.8.0');
|
api.use('vulcan:core@1.8.5');
|
||||||
|
|
||||||
api.use('ecmascript');
|
api.use('ecmascript');
|
||||||
api.use('tracker');
|
api.use('tracker');
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: "vulcan:admin",
|
name: "vulcan:admin",
|
||||||
summary: "Vulcan components package",
|
summary: "Vulcan components package",
|
||||||
version: '1.8.0',
|
version: '1.8.5',
|
||||||
git: "https://github.com/VulcanJS/Vulcan.git"
|
git: "https://github.com/VulcanJS/Vulcan.git"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ Package.onUse(function (api) {
|
||||||
'fourseven:scss@4.5.0',
|
'fourseven:scss@4.5.0',
|
||||||
'dynamic-import@0.1.1',
|
'dynamic-import@0.1.1',
|
||||||
// Vulcan packages
|
// Vulcan packages
|
||||||
'vulcan:core@1.8.0',
|
'vulcan:core@1.8.5',
|
||||||
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: 'vulcan:cloudinary',
|
name: 'vulcan:cloudinary',
|
||||||
summary: 'Vulcan file upload package.',
|
summary: 'Vulcan file upload package.',
|
||||||
version: '1.8.0',
|
version: '1.8.5',
|
||||||
git: "https://github.com/VulcanJS/Vulcan.git"
|
git: "https://github.com/VulcanJS/Vulcan.git"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ Package.onUse(function (api) {
|
||||||
api.versionsFrom('METEOR@1.5.2');
|
api.versionsFrom('METEOR@1.5.2');
|
||||||
|
|
||||||
api.use([
|
api.use([
|
||||||
'vulcan:core@1.8.0'
|
'vulcan:core@1.8.5'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
api.mainModule("lib/client/main.js", "client");
|
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} Redux store reference instantiated on the current connected client
|
||||||
* @param {Object} Apollo Client 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());
|
store.dispatch(getActions().messages.clearSeen());
|
||||||
|
|
||||||
return unusedItem;
|
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 React, { PureComponent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
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';
|
import withCurrentUser from '../containers/withCurrentUser.js';
|
||||||
|
|
||||||
class App extends PureComponent {
|
class App extends PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
if (props.currentUser) {
|
||||||
|
runCallbacks('events.identify', props.currentUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getLocale() {
|
getLocale() {
|
||||||
return getSetting('locale', 'en');
|
return getSetting('locale', 'en');
|
||||||
}
|
}
|
||||||
|
|
||||||
getChildContext() {
|
getChildContext() {
|
||||||
|
|
||||||
const messages = Strings[this.getLocale()] || {};
|
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();
|
const { intl } = intlProvider.getChildContext();
|
||||||
return {
|
return {
|
||||||
intl: intl
|
intl: intl,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUpdate(nextProps) {
|
||||||
|
if (!this.props.currentUser && nextProps.currentUser) {
|
||||||
|
runCallbacks('events.identify', nextProps.currentUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
||||||
const currentRoute = _.last(this.props.routes);
|
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 (
|
return (
|
||||||
<IntlProvider locale={this.getLocale()} messages={Strings[this.getLocale()]}>
|
<IntlProvider
|
||||||
|
locale={this.getLocale()}
|
||||||
|
messages={Strings[this.getLocale()]}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<Components.HeadTags />
|
<Components.HeadTags />
|
||||||
|
<Components.RouterHook currentRoute={currentRoute} />
|
||||||
<LayoutComponent {...this.props} 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>
|
</LayoutComponent>
|
||||||
</div>
|
</div>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
|
@ -40,11 +72,11 @@ class App extends PureComponent {
|
||||||
|
|
||||||
App.propTypes = {
|
App.propTypes = {
|
||||||
currentUserLoading: PropTypes.bool,
|
currentUserLoading: PropTypes.bool,
|
||||||
}
|
};
|
||||||
|
|
||||||
App.childContextTypes = {
|
App.childContextTypes = {
|
||||||
intl: intlShape,
|
intl: intlShape,
|
||||||
}
|
};
|
||||||
|
|
||||||
App.displayName = 'App';
|
App.displayName = 'App';
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ const getTypeName = (field, fieldName, collection) => {
|
||||||
const parseImageUrl = value => {
|
const parseImageUrl = value => {
|
||||||
const isImage = ['.png', '.jpg', '.gif'].indexOf(value.substr(-4)) !== -1 || ['.webp', '.jpeg' ].indexOf(value.substr(-5)) !== -1;
|
const isImage = ['.png', '.jpg', '.gif'].indexOf(value.substr(-4)) !== -1 || ['.webp', '.jpeg' ].indexOf(value.substr(-5)) !== -1;
|
||||||
return isImage ?
|
return isImage ?
|
||||||
<img style={{width: '100%', maxWidth: 200}} src={value} alt={value}/> :
|
<img style={{width: '100%', minWidth: 80, maxWidth: 200, display: 'block'}} src={value} alt={value}/> :
|
||||||
<LimitedString string={value}/>;
|
<LimitedString string={value}/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ export const getFieldValue = (value, typeName) => {
|
||||||
case 'Object':
|
case 'Object':
|
||||||
case 'object':
|
case 'object':
|
||||||
return (
|
return (
|
||||||
<table className="table">
|
<table className="table table-bordered">
|
||||||
<tbody>
|
<tbody>
|
||||||
{_.map(value, (value, key) =>
|
{_.map(value, (value, key) =>
|
||||||
<tr key={key}>
|
<tr key={key}>
|
||||||
|
|
|
@ -56,7 +56,8 @@ class Datatable extends PureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`datatable datatable-${this.props.collection._name}`}>
|
<div className={`datatable datatable-${this.props.collection._name}`}>
|
||||||
{this.props.showSearch ? <input className="datatable-search form-control" placeholder="Search…" type="text" name="datatableSearchQuery" value={this.state.value} onChange={this.updateQuery} /> : null}
|
{this.props.showSearch && <input className="datatable-search form-control" placeholder="Search…" type="text" name="datatableSearchQuery" value={this.state.value} onChange={this.updateQuery} />}
|
||||||
|
{this.props.showNew && <Components.NewButton collection={this.props.collection}/>}
|
||||||
<DatatableWithList {...this.props} terms={{query: this.state.query}} currentUser={this.props.currentUser}/>
|
<DatatableWithList {...this.props} terms={{query: this.state.query}} currentUser={this.props.currentUser}/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -68,10 +69,12 @@ Datatable.propTypes = {
|
||||||
columns: PropTypes.array,
|
columns: PropTypes.array,
|
||||||
options: PropTypes.object,
|
options: PropTypes.object,
|
||||||
showEdit: PropTypes.bool,
|
showEdit: PropTypes.bool,
|
||||||
|
showNew: PropTypes.bool,
|
||||||
showSearch: PropTypes.bool,
|
showSearch: PropTypes.bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
Datatable.defaultProps = {
|
Datatable.defaultProps = {
|
||||||
|
showNew: true,
|
||||||
showEdit: true,
|
showEdit: true,
|
||||||
showSearch: true,
|
showSearch: true,
|
||||||
}
|
}
|
||||||
|
@ -89,11 +92,11 @@ const DatatableHeader = ({ collection, column }, { intl }) => {
|
||||||
/*
|
/*
|
||||||
|
|
||||||
use either:
|
use either:
|
||||||
|
|
||||||
1. the column name translation
|
1. the column name translation
|
||||||
2. the column name label in the schema (if the column name matches a schema field)
|
2. the column name label in the schema (if the column name matches a schema field)
|
||||||
3. the raw column name.
|
3. the raw column name.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
const formattedLabel = intl.formatMessage({ id: `${collection._name}.${columnName}`, defaultMessage: schema[columnName] ? schema[columnName].label : columnName });
|
const formattedLabel = intl.formatMessage({ id: `${collection._name}.${columnName}`, defaultMessage: schema[columnName] ? schema[columnName].label : columnName });
|
||||||
return <th>{formattedLabel}</th>;
|
return <th>{formattedLabel}</th>;
|
||||||
|
@ -112,10 +115,12 @@ DatatableContents Component
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const DatatableContents = (props) => {
|
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) {
|
if (loading) {
|
||||||
return <Components.Loading />;
|
return <Components.Loading />;
|
||||||
|
} else if (!results.length) {
|
||||||
|
return emptyState || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLoadingMore = networkStatus === 2;
|
const isLoadingMore = networkStatus === 2;
|
||||||
|
@ -123,26 +128,26 @@ const DatatableContents = (props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="datatable-list">
|
<div className="datatable-list">
|
||||||
<table className="table">
|
<table className="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{_.sortBy(columns, column => column.order).map((column, index) => <Components.DatatableHeader key={index} collection={collection} column={column}/>)}
|
{_.sortBy(columns, column => column.order).map((column, index) => <Components.DatatableHeader key={index} collection={collection} column={column}/>)}
|
||||||
{showEdit ? <th><FormattedMessage id="datatable.edit"/></th> : null}
|
{showEdit ? <th><FormattedMessage id="datatable.edit"/></th> : null}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{results.map((document, index) => <Components.DatatableRow collection={collection} columns={columns} document={document} key={index} showEdit={showEdit} currentUser={currentUser}/>)}
|
{results.map((document, index) => <Components.DatatableRow collection={collection} columns={columns} document={document} key={index} showEdit={showEdit} currentUser={currentUser}/>)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div className="admin-users-load-more">
|
<div className="admin-users-load-more">
|
||||||
{hasMore ?
|
{hasMore ?
|
||||||
isLoadingMore ?
|
isLoadingMore ?
|
||||||
<Components.Loading/>
|
<Components.Loading/>
|
||||||
: <Button bsStyle="primary" onClick={e => {e.preventDefault(); loadMore();}}>Load More ({count}/{totalCount})</Button>
|
: <Button bsStyle="primary" onClick={e => {e.preventDefault(); loadMore();}}>Load More ({count}/{totalCount})</Button>
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
registerComponent('DatatableContents', DatatableContents);
|
registerComponent('DatatableContents', DatatableContents);
|
||||||
|
@ -157,11 +162,11 @@ const DatatableRow = ({ collection, columns, document, showEdit, currentUser },
|
||||||
<tr className="datatable-item">
|
<tr className="datatable-item">
|
||||||
|
|
||||||
{_.sortBy(columns, column => column.order).map((column, index) => <Components.DatatableCell key={index} column={column} document={document} currentUser={currentUser} />)}
|
{_.sortBy(columns, column => column.order).map((column, index) => <Components.DatatableCell key={index} column={column} document={document} currentUser={currentUser} />)}
|
||||||
|
|
||||||
{showEdit ?
|
{showEdit ?
|
||||||
<td>
|
<td>
|
||||||
<Components.ModalTrigger
|
<Components.ModalTrigger
|
||||||
label={intl.formatMessage({id: 'datatable.edit'})}
|
label={intl.formatMessage({id: 'datatable.edit'})}
|
||||||
component={<Button bsStyle="primary"><FormattedMessage id="datatable.edit" /></Button>}
|
component={<Button bsStyle="primary"><FormattedMessage id="datatable.edit" /></Button>}
|
||||||
>
|
>
|
||||||
<Components.DatatableEditForm collection={collection} document={document} />
|
<Components.DatatableEditForm collection={collection} document={document} />
|
||||||
|
@ -183,16 +188,33 @@ DatatableEditForm Component
|
||||||
|
|
||||||
*/
|
*/
|
||||||
const DatatableEditForm = ({ collection, document, closeModal }) =>
|
const DatatableEditForm = ({ collection, document, closeModal }) =>
|
||||||
<Components.SmartForm
|
<Components.SmartForm
|
||||||
collection={collection}
|
collection={collection}
|
||||||
documentId={document._id}
|
documentId={document._id}
|
||||||
showRemove={true}
|
showRemove={true}
|
||||||
successCallback={document => {
|
successCallback={document => {
|
||||||
closeModal();
|
closeModal();
|
||||||
}}
|
}}
|
||||||
|
removeSuccessCallback={document => {
|
||||||
|
closeModal();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
registerComponent('DatatableEditForm', DatatableEditForm);
|
registerComponent('DatatableEditForm', DatatableEditForm);
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
DatatableNewForm Component
|
||||||
|
|
||||||
|
*/
|
||||||
|
const DatatableNewForm = ({ collection, closeModal }) =>
|
||||||
|
<Components.SmartForm
|
||||||
|
collection={collection}
|
||||||
|
successCallback={document => {
|
||||||
|
closeModal();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
registerComponent('DatatableNewForm', DatatableNewForm);
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
|
|
20
packages/vulcan-core/lib/modules/components/EditButton.jsx
Normal file
20
packages/vulcan-core/lib/modules/components/EditButton.jsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { Components, registerComponent } from 'meteor/vulcan:lib';
|
||||||
|
import React from 'react';
|
||||||
|
import Button from 'react-bootstrap/lib/Button';
|
||||||
|
import { FormattedMessage, intlShape } from 'meteor/vulcan:i18n';
|
||||||
|
|
||||||
|
const EditButton = ({ collection, document, bsStyle = 'primary' }, {intl}) =>
|
||||||
|
<Components.ModalTrigger
|
||||||
|
label={intl.formatMessage({id: 'datatable.edit'})}
|
||||||
|
component={<Button bsStyle={bsStyle}><FormattedMessage id="datatable.edit" /></Button>}
|
||||||
|
>
|
||||||
|
<Components.DatatableEditForm collection={collection} document={document} />
|
||||||
|
</Components.ModalTrigger>
|
||||||
|
|
||||||
|
EditButton.contextTypes = {
|
||||||
|
intl: intlShape
|
||||||
|
};
|
||||||
|
|
||||||
|
EditButton.displayName = 'EditButton';
|
||||||
|
|
||||||
|
registerComponent('EditButton', EditButton);
|
|
@ -2,6 +2,7 @@ import React, { PureComponent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Helmet from 'react-helmet';
|
import Helmet from 'react-helmet';
|
||||||
import { registerComponent, Utils, getSetting, registerSetting, Head } from 'meteor/vulcan:lib';
|
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('logoUrl', null, 'Absolute URL for the logo image');
|
||||||
registerSetting('title', 'My App', 'App title');
|
registerSetting('title', 'My App', 'App title');
|
||||||
|
@ -13,9 +14,9 @@ registerSetting('faviconUrl', '/img/favicon.ico', 'Favicon absolute URL');
|
||||||
class HeadTags extends PureComponent {
|
class HeadTags extends PureComponent {
|
||||||
render() {
|
render() {
|
||||||
|
|
||||||
const url = !!this.props.url ? this.props.url : Utils.getSiteUrl();
|
const url = this.props.url || Utils.getSiteUrl();
|
||||||
const title = !!this.props.title ? this.props.title : getSetting('title', 'My App');
|
const title = this.props.title || getSetting('title', 'My App');
|
||||||
const description = !!this.props.description ? this.props.description : getSetting('tagline') || getSetting('description');
|
const description = this.props.description || getSetting('tagline') || getSetting('description');
|
||||||
|
|
||||||
// default image meta: logo url, else site image defined in settings
|
// default image meta: logo url, else site image defined in settings
|
||||||
let image = !!getSetting('siteImage') ? getSetting('siteImage'): getSetting('logoUrl');
|
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
|
// add site url base if the image is stored locally
|
||||||
if (!!image && image.indexOf('//') === -1) {
|
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;
|
image = Utils.getSiteUrl() + image;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,9 +63,21 @@ class HeadTags extends PureComponent {
|
||||||
|
|
||||||
{Head.meta.map((tag, index) => <meta key={index} {...tag}/>)}
|
{Head.meta.map((tag, index) => <meta key={index} {...tag}/>)}
|
||||||
{Head.link.map((tag, index) => <link 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}>{tag.contents}</script>)}
|
||||||
|
|
||||||
</Helmet>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
||||||
|
|
||||||
const Loading = props => {
|
const Loading = props => {
|
||||||
return (
|
return (
|
||||||
<div className="spinner">
|
<div className={`spinner ${props.className}`}>
|
||||||
<div className="bounce1"></div>
|
<div className="bounce1"></div>
|
||||||
<div className="bounce2"></div>
|
<div className="bounce2"></div>
|
||||||
<div className="bounce3"></div>
|
<div className="bounce3"></div>
|
||||||
|
|
20
packages/vulcan-core/lib/modules/components/NewButton.jsx
Normal file
20
packages/vulcan-core/lib/modules/components/NewButton.jsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { Components, registerComponent } from 'meteor/vulcan:lib';
|
||||||
|
import React from 'react';
|
||||||
|
import Button from 'react-bootstrap/lib/Button';
|
||||||
|
import { FormattedMessage, intlShape } from 'meteor/vulcan:i18n';
|
||||||
|
|
||||||
|
const NewButton = ({ collection, bsStyle = 'primary' }, {intl}) =>
|
||||||
|
<Components.ModalTrigger
|
||||||
|
label={intl.formatMessage({id: 'datatable.new'})}
|
||||||
|
component={<Button bsStyle={bsStyle}><FormattedMessage id="datatable.new" /></Button>}
|
||||||
|
>
|
||||||
|
<Components.DatatableNewForm collection={collection} />
|
||||||
|
</Components.ModalTrigger>
|
||||||
|
|
||||||
|
NewButton.contextTypes = {
|
||||||
|
intl: intlShape
|
||||||
|
};
|
||||||
|
|
||||||
|
NewButton.displayName = 'NewButton';
|
||||||
|
|
||||||
|
registerComponent('NewButton', NewButton);
|
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);
|
36
packages/vulcan-core/lib/modules/containers/withAccess.js
Normal file
36
packages/vulcan-core/lib/modules/containers/withAccess.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { withCurrentUser } from 'meteor/vulcan:core';
|
||||||
|
import { withRouter } from 'react-router';
|
||||||
|
import Users from 'meteor/vulcan:users';
|
||||||
|
|
||||||
|
export default function withAccess (options) {
|
||||||
|
|
||||||
|
const { groups, redirect } = options;
|
||||||
|
|
||||||
|
// we return a function that takes a component and itself returns a component
|
||||||
|
return WrappedComponent => {
|
||||||
|
class AccessComponent extends PureComponent {
|
||||||
|
|
||||||
|
// if there are any groups defined check if user belongs, else just check if user exists
|
||||||
|
canAcces = currentUser => {
|
||||||
|
return groups ? currentUser && Users.isMemberOf(currentUser, groups) : currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
// redirect on constructor if user cannot access
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
if(!this.canAcces(props.currentUser)) {
|
||||||
|
props.router.push(redirect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return this.canAcces(this.props.currentUser) ? <WrappedComponent/> : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AccessComponent.displayName = `withAccess(${WrappedComponent.displayName})`;
|
||||||
|
|
||||||
|
return withRouter(withCurrentUser(AccessComponent));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,12 @@
|
||||||
import React, { PropTypes, Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { graphql } from 'react-apollo';
|
import { graphql } from 'react-apollo';
|
||||||
import gql from 'graphql-tag';
|
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) {
|
export default function withDocument (options) {
|
||||||
|
|
||||||
const { collection, pollInterval = 20000 } = options,
|
const { collection, pollInterval = getSetting('pollInterval', 20000), enableCache = false, extraQueries } = options,
|
||||||
queryName = options.queryName || `${collection.options.collectionName}SingleQuery`,
|
queryName = options.queryName || `${collection.options.collectionName}SingleQuery`,
|
||||||
singleResolverName = collection.options.resolvers.single && collection.options.resolvers.single.name;
|
singleResolverName = collection.options.resolvers.single && collection.options.resolvers.single.name;
|
||||||
|
|
||||||
|
@ -22,31 +23,48 @@ export default function withDocument (options) {
|
||||||
const fragmentName = getFragmentName(fragment);
|
const fragmentName = getFragmentName(fragment);
|
||||||
|
|
||||||
return graphql(gql`
|
return graphql(gql`
|
||||||
query ${queryName}($documentId: String, $slug: String) {
|
query ${queryName}($documentId: String, $slug: String, $enableCache: Boolean) {
|
||||||
${singleResolverName}(documentId: $documentId, slug: $slug) {
|
${singleResolverName}(documentId: $documentId, slug: $slug, enableCache: $enableCache) {
|
||||||
__typename
|
__typename
|
||||||
...${fragmentName}
|
...${fragmentName}
|
||||||
}
|
}
|
||||||
|
${extraQueries || ''}
|
||||||
}
|
}
|
||||||
${fragment}
|
${fragment}
|
||||||
`, {
|
`, {
|
||||||
alias: 'withDocument',
|
alias: 'withDocument',
|
||||||
|
|
||||||
options(ownProps) {
|
options(ownProps) {
|
||||||
return {
|
const graphQLOptions = {
|
||||||
variables: { documentId: ownProps.documentId, slug: ownProps.slug },
|
variables: { documentId: ownProps.documentId, slug: ownProps.slug, enableCache },
|
||||||
pollInterval, // note: pollInterval can be set to 0 to disable polling (20s by default)
|
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 => {
|
props: returnedProps => {
|
||||||
const { ownProps, data } = returnedProps;
|
const { ownProps, data } = returnedProps;
|
||||||
return {
|
|
||||||
|
const propertyName = options.propertyName || 'document';
|
||||||
|
const props = {
|
||||||
loading: data.loading,
|
loading: data.loading,
|
||||||
// document: Utils.convertDates(collection, data[singleResolverName]),
|
// document: Utils.convertDates(collection, data[singleResolverName]),
|
||||||
document: data[singleResolverName],
|
[ propertyName ]: data[singleResolverName],
|
||||||
fragmentName,
|
fragmentName,
|
||||||
fragment,
|
fragment,
|
||||||
|
data,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
// get graphQL error (see https://github.com/thebigredgeek/apollo-errors/issues/12)
|
||||||
|
props.error = data.error.graphQLErrors[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return props;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,11 +34,12 @@ Terms object can have the following properties:
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { PropTypes, Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { withApollo, graphql } from 'react-apollo';
|
import { withApollo, graphql } from 'react-apollo';
|
||||||
import gql from 'graphql-tag';
|
import gql from 'graphql-tag';
|
||||||
import update from 'immutability-helper';
|
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 Mingo from 'mingo';
|
||||||
import compose from 'recompose/compose';
|
import compose from 'recompose/compose';
|
||||||
import withState from 'recompose/withState';
|
import withState from 'recompose/withState';
|
||||||
|
@ -47,7 +48,7 @@ const withList = (options) => {
|
||||||
|
|
||||||
// console.log(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, extraQueries } = options,
|
||||||
queryName = options.queryName || `${collection.options.collectionName}ListQuery`,
|
queryName = options.queryName || `${collection.options.collectionName}ListQuery`,
|
||||||
listResolverName = collection.options.resolvers.list && collection.options.resolvers.list.name,
|
listResolverName = collection.options.resolvers.list && collection.options.resolvers.list.name,
|
||||||
totalResolverName = collection.options.resolvers.total && collection.options.resolvers.total.name;
|
totalResolverName = collection.options.resolvers.total && collection.options.resolvers.total.name;
|
||||||
|
@ -66,12 +67,13 @@ const withList = (options) => {
|
||||||
|
|
||||||
// build graphql query from options
|
// build graphql query from options
|
||||||
const query = gql`
|
const query = gql`
|
||||||
query ${queryName}($terms: JSON) {
|
query ${queryName}($terms: JSON, $enableCache: Boolean) {
|
||||||
${totalResolver ? `${totalResolverName}(terms: $terms)` : ``}
|
${totalResolver ? `${totalResolverName}(terms: $terms, enableCache: $enableCache)` : ``}
|
||||||
${listResolverName}(terms: $terms) {
|
${listResolverName}(terms: $terms, enableCache: $enableCache) {
|
||||||
__typename
|
__typename
|
||||||
...${fragmentName}
|
...${fragmentName}
|
||||||
}
|
}
|
||||||
|
${extraQueries || ''}
|
||||||
}
|
}
|
||||||
${fragment}
|
${fragment}
|
||||||
`;
|
`;
|
||||||
|
@ -106,9 +108,11 @@ const withList = (options) => {
|
||||||
options({terms, paginationTerms, client: apolloClient}) {
|
options({terms, paginationTerms, client: apolloClient}) {
|
||||||
// get terms from options, then props, then pagination
|
// get terms from options, then props, then pagination
|
||||||
const mergedTerms = {...options.terms, ...terms, ...paginationTerms};
|
const mergedTerms = {...options.terms, ...terms, ...paginationTerms};
|
||||||
return {
|
|
||||||
|
const graphQLOptions = {
|
||||||
variables: {
|
variables: {
|
||||||
terms: mergedTerms,
|
terms: mergedTerms,
|
||||||
|
enableCache,
|
||||||
},
|
},
|
||||||
// note: pollInterval can be set to 0 to disable polling (20s by default)
|
// note: pollInterval can be set to 0 to disable polling (20s by default)
|
||||||
pollInterval,
|
pollInterval,
|
||||||
|
@ -119,18 +123,27 @@ const withList = (options) => {
|
||||||
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (options.fetchPolicy) {
|
||||||
|
graphQLOptions.fetchPolicy = options.fetchPolicy
|
||||||
|
}
|
||||||
|
|
||||||
|
return graphQLOptions;
|
||||||
},
|
},
|
||||||
|
|
||||||
// define props returned by graphql HoC
|
// define props returned by graphql HoC
|
||||||
props(props) {
|
props(props) {
|
||||||
|
|
||||||
|
// see https://github.com/apollographql/apollo-client/blob/master/packages/apollo-client/src/core/networkStatus.ts
|
||||||
const refetch = props.data.refetch,
|
const refetch = props.data.refetch,
|
||||||
// results = Utils.convertDates(collection, props.data[listResolverName]),
|
// results = Utils.convertDates(collection, props.data[listResolverName]),
|
||||||
results = props.data[listResolverName],
|
results = props.data[listResolverName],
|
||||||
totalCount = props.data[totalResolverName],
|
totalCount = props.data[totalResolverName],
|
||||||
networkStatus = props.data.networkStatus,
|
networkStatus = props.data.networkStatus,
|
||||||
loading = props.data.loading,
|
loading = props.data.networkStatus === 1,
|
||||||
error = props.data.error;
|
loadingMore = props.data.networkStatus === 2,
|
||||||
|
error = props.data.error,
|
||||||
|
propertyName = options.propertyName || 'results';
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
@ -139,8 +152,9 @@ const withList = (options) => {
|
||||||
return {
|
return {
|
||||||
// see https://github.com/apollostack/apollo-client/blob/master/src/queries/store.ts#L28-L36
|
// 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
|
// note: loading will propably change soon https://github.com/apollostack/apollo-client/issues/831
|
||||||
loading: networkStatus === 1,
|
loading,
|
||||||
results,
|
loadingMore,
|
||||||
|
[ propertyName ]: results,
|
||||||
totalCount,
|
totalCount,
|
||||||
refetch,
|
refetch,
|
||||||
networkStatus,
|
networkStatus,
|
||||||
|
@ -179,7 +193,8 @@ const withList = (options) => {
|
||||||
|
|
||||||
fragmentName,
|
fragmentName,
|
||||||
fragment,
|
fragment,
|
||||||
...props.ownProps // pass on the props down to the wrapped component
|
...props.ownProps, // pass on the props down to the wrapped component
|
||||||
|
data: props.data,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,121 +4,219 @@ 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';
|
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: {
|
return {
|
||||||
|
|
||||||
name: `${collectionName}New`,
|
// mutation for inserting a new document
|
||||||
|
|
||||||
// check function called on a user to see if they can perform the operation
|
new: {
|
||||||
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];
|
name: `${collectionName}New`,
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user || !document) return false;
|
description: `Mutation for inserting new ${collectionName} documents`,
|
||||||
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`,
|
||||||
|
|
||||||
|
description: `Mutation for editing a ${collectionName} document`,
|
||||||
|
|
||||||
|
// 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);
|
name: `${collectionName}Remove`,
|
||||||
Utils.performCheck(this.check, context.currentUser, document, context);
|
|
||||||
|
description: `Mutation for deleting a ${collectionName} document`,
|
||||||
|
|
||||||
|
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.`
|
||||||
|
});
|
||||||
|
}
|
|
@ -4,99 +4,141 @@ Default list, single, and total resolvers
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Utils, debug } from 'meteor/vulcan:core';
|
import { Utils, debug, debugGroup, debugGroupEnd } from 'meteor/vulcan:core';
|
||||||
|
import { createError } from 'apollo-errors';
|
||||||
|
|
||||||
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 ---------------//`);
|
name: `${collectionName}List`,
|
||||||
debug(terms);
|
|
||||||
|
|
||||||
// get currentUser and Users collection from context
|
description: `A list of ${collectionName} documents matching a set of query terms`,
|
||||||
const { currentUser, Users } = context;
|
|
||||||
|
|
||||||
// get collection based on collectionName argument
|
async resolver(root, { terms = {}, enableCache = false }, context, { cacheControl }) {
|
||||||
const collection = context[collectionName];
|
debug('')
|
||||||
|
debugGroup(`--------------- start \x1b[35m${collectionName} list\x1b[0m resolver ---------------`);
|
||||||
|
debug(`Options: ${JSON.stringify(resolverOptions)}`);
|
||||||
|
debug(`Terms: ${JSON.stringify(terms)}`);
|
||||||
|
|
||||||
// get selector and options from terms and perform Mongo query
|
if (cacheControl && enableCache) {
|
||||||
let {selector, options} = await collection.getParameters(terms, {}, context);
|
const maxAge = resolverOptions.cacheMaxAge || defaultOptions.cacheMaxAge;
|
||||||
options.skip = terms.offset;
|
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
|
// get selector and options from terms and perform Mongo query
|
||||||
const viewableDocs = collection.checkAccess ? _.filter(docs, doc => collection.checkAccess(currentUser, doc)) : docs;
|
let { selector, options } = await collection.getParameters(terms, {}, context);
|
||||||
|
options.skip = terms.offset;
|
||||||
// take the remaining documents and remove any fields that shouldn't be accessible
|
|
||||||
const restrictedDocs = Users.restrictViewableFields(currentUser, collection, viewableDocs);
|
|
||||||
|
|
||||||
// prime the cache
|
debug({ selector, options });
|
||||||
restrictedDocs.forEach(doc => collection.loader.prime(doc._id, doc));
|
|
||||||
|
|
||||||
debug(`// ${restrictedDocs.length} documents returned`);
|
const docs = collection.find(selector, options).fetch();
|
||||||
debug(`//--------------- end ${collectionName} list resolver ---------------//`);
|
|
||||||
|
// 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(`\x1b[33m=> ${restrictedDocs.length} documents returned\x1b[0m`);
|
||||||
|
debugGroupEnd();
|
||||||
|
debug(`--------------- end \x1b[35m${collectionName} list\x1b[0m resolver ---------------`);
|
||||||
|
debug('')
|
||||||
|
|
||||||
|
// return results
|
||||||
|
return restrictedDocs;
|
||||||
|
},
|
||||||
|
|
||||||
// return results
|
|
||||||
return restrictedDocs;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
},
|
// resolver for returning a single document queried based on id or slug
|
||||||
|
|
||||||
// resolver for returning a single document queried based on id or slug
|
single: {
|
||||||
|
|
||||||
single: {
|
name: `${collectionName}Single`,
|
||||||
|
|
||||||
name: `${collectionName}Single`,
|
|
||||||
|
|
||||||
async resolver(root, {documentId, slug}, context) {
|
description: `A single ${collectionName} document fetched by ID or slug`,
|
||||||
|
|
||||||
debug(`//--------------- start ${collectionName} single resolver ---------------//`);
|
async resolver(root, { documentId, slug, enableCache = false }, context, { cacheControl }) {
|
||||||
debug(documentId);
|
debug('')
|
||||||
|
debugGroup(`--------------- start \x1b[35m${collectionName} single\x1b[0m resolver ---------------`);
|
||||||
|
debug(`Options: ${JSON.stringify(resolverOptions)}`);
|
||||||
|
debug(`DocumentId: ${documentId}`);
|
||||||
|
|
||||||
const { currentUser, Users } = context;
|
if (cacheControl && enableCache) {
|
||||||
const collection = context[collectionName];
|
const maxAge = resolverOptions.cacheMaxAge || defaultOptions.cacheMaxAge;
|
||||||
|
cacheControl.setCacheHint({ maxAge });
|
||||||
|
}
|
||||||
|
|
||||||
// don't use Dataloader if doc is selected by slug
|
const { currentUser, Users } = context;
|
||||||
const doc = documentId ? await collection.loader.load(documentId) : (slug ? collection.findOne({slug}) : collection.findOne());
|
const collection = context[collectionName];
|
||||||
|
|
||||||
// if collection has a checkAccess function defined, use it to perform a check on the current document
|
// don't use Dataloader if doc is selected by slug
|
||||||
// (will throw an error if check doesn't pass)
|
const doc = documentId ? await collection.loader.load(documentId) : (slug ? collection.findOne({ slug }) : collection.findOne());
|
||||||
if (collection.checkAccess) {
|
|
||||||
Utils.performCheck(collection.checkAccess, currentUser, doc, collection, documentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
debug(`//--------------- end ${collectionName} single resolver ---------------//`);
|
if (!doc) {
|
||||||
|
const MissingDocumentError = createError('app.missing_document', { message: 'app.missing_document' });
|
||||||
|
throw new MissingDocumentError({ data: { documentId, slug } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
debugGroupEnd();
|
||||||
|
debug(`--------------- end \x1b[35m${collectionName} single\x1b[0m resolver ---------------`);
|
||||||
|
debug('')
|
||||||
|
|
||||||
|
// filter out disallowed properties and return resulting document
|
||||||
|
return restrictedDoc;
|
||||||
|
},
|
||||||
|
|
||||||
// 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
|
// resolver for returning the total number of documents matching a set of query terms
|
||||||
|
|
||||||
total: {
|
total: {
|
||||||
|
|
||||||
name: `${collectionName}Total`,
|
|
||||||
|
|
||||||
async resolver(root, {terms}, context) {
|
|
||||||
|
|
||||||
const collection = context[collectionName];
|
|
||||||
|
|
||||||
const {selector} = await collection.getParameters(terms, {}, context);
|
name: `${collectionName}Total`,
|
||||||
|
|
||||||
return collection.find(selector).count();
|
description: `The total count of ${collectionName} documents matching a set of query terms`,
|
||||||
},
|
|
||||||
|
async resolver(root, { terms, enableCache }, context, { cacheControl }) {
|
||||||
|
|
||||||
|
if (cacheControl && enableCache) {
|
||||||
|
const maxAge = resolverOptions.cacheMaxAge || defaultOptions.cacheMaxAge;
|
||||||
|
cacheControl.setCacheHint({ maxAge });
|
||||||
|
}
|
||||||
|
|
||||||
|
const collection = context[collectionName];
|
||||||
|
|
||||||
|
const { selector } = await collection.getParameters(terms, {}, context);
|
||||||
|
|
||||||
|
return collection.find(selector).count();
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
};
|
||||||
|
|
|
@ -12,6 +12,8 @@ export { default as Icon } from "./components/Icon.jsx";
|
||||||
export { default as Loading } from "./components/Loading.jsx";
|
export { default as Loading } from "./components/Loading.jsx";
|
||||||
export { default as ShowIf } from "./components/ShowIf.jsx";
|
export { default as ShowIf } from "./components/ShowIf.jsx";
|
||||||
export { default as ModalTrigger } from './components/ModalTrigger.jsx';
|
export { default as ModalTrigger } from './components/ModalTrigger.jsx';
|
||||||
|
export { default as NewButton } from './components/NewButton.jsx';
|
||||||
|
export { default as EditButton } from './components/EditButton.jsx';
|
||||||
export { default as Error404 } from './components/Error404.jsx';
|
export { default as Error404 } from './components/Error404.jsx';
|
||||||
export { default as DynamicLoading } from './components/DynamicLoading.jsx';
|
export { default as DynamicLoading } from './components/DynamicLoading.jsx';
|
||||||
export { default as HeadTags } from './components/HeadTags.jsx';
|
export { default as HeadTags } from './components/HeadTags.jsx';
|
||||||
|
@ -21,7 +23,9 @@ export { default as Datatable } from './components/Datatable.jsx';
|
||||||
export { default as Flash } from './components/Flash.jsx';
|
export { default as Flash } from './components/Flash.jsx';
|
||||||
export { default as HelloWorld } from './components/HelloWorld.jsx';
|
export { default as HelloWorld } from './components/HelloWorld.jsx';
|
||||||
export { default as Welcome } from './components/Welcome.jsx';
|
export { default as Welcome } from './components/Welcome.jsx';
|
||||||
|
export { default as RouterHook } from './components/RouterHook.jsx';
|
||||||
|
|
||||||
|
export { default as withAccess } from "./containers/withAccess.js";
|
||||||
export { default as withMessages } from "./containers/withMessages.js";
|
export { default as withMessages } from "./containers/withMessages.js";
|
||||||
export { default as withList } from './containers/withList.js';
|
export { default as withList } from './containers/withList.js';
|
||||||
export { default as withDocument } from './containers/withDocument.js';
|
export { default as withDocument } from './containers/withDocument.js';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: "vulcan:core",
|
name: "vulcan:core",
|
||||||
summary: "Vulcan core package",
|
summary: "Vulcan core package",
|
||||||
version: '1.8.0',
|
version: '1.8.5',
|
||||||
git: "https://github.com/VulcanJS/Vulcan.git"
|
git: "https://github.com/VulcanJS/Vulcan.git"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -10,15 +10,15 @@ Package.onUse(function(api) {
|
||||||
api.versionsFrom('METEOR@1.5.2');
|
api.versionsFrom('METEOR@1.5.2');
|
||||||
|
|
||||||
api.use([
|
api.use([
|
||||||
'vulcan:lib@1.8.0',
|
'vulcan:lib@1.8.5',
|
||||||
'vulcan:i18n@1.8.0',
|
'vulcan:i18n@1.8.5',
|
||||||
'vulcan:users@1.8.0',
|
'vulcan:users@1.8.5',
|
||||||
'vulcan:routing@1.8.0',
|
'vulcan:routing@1.8.5',
|
||||||
'vulcan:debug@1.8.0',
|
'vulcan:debug@1.8.5',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
api.imply([
|
api.imply([
|
||||||
'vulcan:lib@1.8.0'
|
'vulcan:lib@1.8.5'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
api.mainModule('lib/server/main.js', 'server');
|
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>{name}</td>
|
||||||
<td><a href={"/email/template/"+email.template} target="_blank">{email.template}</a></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>{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>
|
<td>
|
||||||
<div className={this.state.loading ? "test-email loading" : "test-email"}>
|
<div className={this.state.loading ? "test-email loading" : "test-email"}>
|
||||||
<Button disabled={this.state.loading} onClick={this.sendTest} bsStyle="primary">Send Test</Button>
|
<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/Emails.jsx';
|
||||||
import '../components/Groups.jsx';
|
import '../components/Groups.jsx';
|
||||||
import '../components/Settings.jsx';
|
import '../components/Settings.jsx';
|
||||||
|
import '../components/Callbacks.jsx';
|
||||||
|
|
|
@ -4,6 +4,7 @@ addRoute([
|
||||||
// {name: 'cheatsheet', path: '/cheatsheet', component: import('./components/Cheatsheet.jsx')},
|
// {name: 'cheatsheet', path: '/cheatsheet', component: import('./components/Cheatsheet.jsx')},
|
||||||
{name: 'groups', path: '/groups', component: () => getDynamicComponent(import('../components/Groups.jsx'))},
|
{name: 'groups', path: '/groups', component: () => getDynamicComponent(import('../components/Groups.jsx'))},
|
||||||
{name: 'settings', path: '/settings', componentName: 'Settings'},
|
{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', component: () => getDynamicComponent(import('./components/Emails.jsx'))},
|
||||||
{name: 'emails', path: '/emails', componentName: 'Emails'},
|
{name: 'emails', path: '/emails', componentName: 'Emails'},
|
||||||
]);
|
]);
|
|
@ -1,7 +1,7 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: "vulcan:debug",
|
name: "vulcan:debug",
|
||||||
summary: "Vulcan debug package",
|
summary: "Vulcan debug package",
|
||||||
version: '1.8.0',
|
version: '1.8.5',
|
||||||
git: "https://github.com/VulcanJS/Vulcan.git",
|
git: "https://github.com/VulcanJS/Vulcan.git",
|
||||||
debugOnly: true
|
debugOnly: true
|
||||||
});
|
});
|
||||||
|
@ -17,8 +17,8 @@ Package.onUse(function (api) {
|
||||||
|
|
||||||
// Vulcan packages
|
// Vulcan packages
|
||||||
|
|
||||||
'vulcan:lib@1.8.0',
|
'vulcan:lib@1.8.5',
|
||||||
'vulcan:email@1.8.0',
|
'vulcan:email@1.8.5',
|
||||||
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ VulcanEmail.addTemplates = templates => {
|
||||||
|
|
||||||
VulcanEmail.getTemplate = templateName => Handlebars.compile(
|
VulcanEmail.getTemplate = templateName => Handlebars.compile(
|
||||||
VulcanEmail.templates[templateName],
|
VulcanEmail.templates[templateName],
|
||||||
{ noEscape: true}
|
{ noEscape: true, strict: true}
|
||||||
);
|
);
|
||||||
|
|
||||||
VulcanEmail.buildTemplate = (htmlContent, optionalProperties = {}) => {
|
VulcanEmail.buildTemplate = (htmlContent, optionalProperties = {}) => {
|
||||||
|
@ -46,9 +46,7 @@ VulcanEmail.buildTemplate = (htmlContent, optionalProperties = {}) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const emailHTML = VulcanEmail.getTemplate("wrapper")(emailProperties);
|
const emailHTML = VulcanEmail.getTemplate("wrapper")(emailProperties);
|
||||||
|
|
||||||
const inlinedHTML = Juice(emailHTML, {preserveMediaQueries: true});
|
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">'
|
const doctype = '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">'
|
||||||
|
|
||||||
return doctype+inlinedHTML;
|
return doctype+inlinedHTML;
|
||||||
|
@ -122,7 +120,7 @@ VulcanEmail.build = async ({ emailName, variables }) => {
|
||||||
|
|
||||||
const subject = typeof email.subject === 'function' ? email.subject(data) : email.subject;
|
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 };
|
return { data, subject, html };
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ Meteor.startup(function () {
|
||||||
Picker.route(email.path, async (params, req, res) => {
|
Picker.route(email.path, async (params, req, res) => {
|
||||||
|
|
||||||
let html;
|
let html;
|
||||||
|
|
||||||
// if email has a custom way of generating test HTML, use it
|
// if email has a custom way of generating test HTML, use it
|
||||||
if (typeof email.getTestHTML !== "undefined") {
|
if (typeof email.getTestHTML !== "undefined") {
|
||||||
|
|
||||||
|
@ -20,14 +19,20 @@ Meteor.startup(function () {
|
||||||
|
|
||||||
// else get test object (sample post, comment, user, etc.)
|
// else get test object (sample post, comment, user, etc.)
|
||||||
const testVariables = (typeof email.testVariables === 'function' ? email.testVariables() : email.testVariables) || {};
|
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
|
// 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 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
|
// 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 += `
|
html += `
|
||||||
<h4 style="margin: 20px;"><code>Subject: ${subject}</code></h4>
|
<h4 style="margin: 20px;"><code>Subject: ${subject}</code></h4>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: "vulcan:email",
|
name: "vulcan:email",
|
||||||
summary: "Vulcan email package",
|
summary: "Vulcan email package",
|
||||||
version: '1.8.0',
|
version: '1.8.5',
|
||||||
git: "https://github.com/VulcanJS/Vulcan.git"
|
git: "https://github.com/VulcanJS/Vulcan.git"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ Package.onUse(function (api) {
|
||||||
api.versionsFrom('METEOR@1.5.2');
|
api.versionsFrom('METEOR@1.5.2');
|
||||||
|
|
||||||
api.use([
|
api.use([
|
||||||
'vulcan:lib@1.8.0'
|
'vulcan:lib@1.8.5'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
api.mainModule("lib/server.js", "server");
|
api.mainModule("lib/server.js", "server");
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Components, registerComponent, Utils, getSetting } from 'meteor/vulcan:core';
|
import { Components, registerComponent, Utils, getSetting } from 'meteor/vulcan:core';
|
||||||
import { withMutation } from 'meteor/vulcan:core';
|
import { withMutation } from 'meteor/vulcan:core';
|
||||||
import React, { PropTypes, Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { FormattedMessage, intlShape } from 'meteor/vulcan:i18n';
|
import { FormattedMessage, intlShape } from 'meteor/vulcan:i18n';
|
||||||
import FRC from 'formsy-react-components';
|
import FRC from 'formsy-react-components';
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
Package.describe({
|
Package.describe({
|
||||||
name: "vulcan:embed",
|
name: "vulcan:embed",
|
||||||
summary: "Vulcan Embed package",
|
summary: "Vulcan Embed package",
|
||||||
version: '1.8.0',
|
version: '1.8.5',
|
||||||
git: 'https://github.com/VulcanJS/Vulcan.git'
|
git: 'https://github.com/VulcanJS/Vulcan.git'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ Package.onUse( function(api) {
|
||||||
api.versionsFrom('METEOR@1.5.2');
|
api.versionsFrom('METEOR@1.5.2');
|
||||||
|
|
||||||
api.use([
|
api.use([
|
||||||
'vulcan:core@1.8.0',
|
'vulcan:core@1.8.5',
|
||||||
'fourseven:scss@4.5.0'
|
'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.5',
|
||||||
|
git: "https://github.com/VulcanJS/Vulcan.git"
|
||||||
|
});
|
||||||
|
|
||||||
|
Package.onUse(function(api) {
|
||||||
|
|
||||||
|
api.versionsFrom('METEOR@1.5.2');
|
||||||
|
|
||||||
|
api.use([
|
||||||
|
'vulcan:core@1.8.5',
|
||||||
|
'vulcan:events@1.8.5',
|
||||||
|
]);
|
||||||
|
|
||||||
|
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,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.5',
|
||||||
|
git: "https://github.com/VulcanJS/Vulcan.git"
|
||||||
|
});
|
||||||
|
|
||||||
|
Package.onUse(function (api) {
|
||||||
|
|
||||||
|
api.versionsFrom('METEOR@1.5.2');
|
||||||
|
|
||||||
|
api.use([
|
||||||
|
'vulcan:core@1.8.5',
|
||||||
|
'vulcan:events@1.8.5'
|
||||||
|
]);
|
||||||
|
|
||||||
|
api.mainModule("lib/client/main.js", "client");
|
||||||
|
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