Merge pull request #1 from VulcanJS/master

Update to 1.8.5
This commit is contained in:
Lucas Martín 2018-01-22 16:26:41 +01:00 committed by GitHub
commit 94d6c29db6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
192 changed files with 9815 additions and 1139 deletions

View file

@ -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

View file

@ -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
View file

@ -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
View file

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

View file

@ -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

View file

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

View file

@ -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
View file

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

View file

@ -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'));

View file

@ -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
View file

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES6"
},
"include": [
"packages/**/*"
],
"exclude": [
"packages/_buffer",
"packages/_boilerplate-generator"
],
"typeAcquisition": {
"enable": true
},
}

6333
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -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",

View file

@ -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');
}); });

View file

@ -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',
]); ]);

View file

@ -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';

View file

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

View file

@ -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';

View file

@ -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';

View file

@ -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');

View file

@ -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');

View file

@ -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;

View file

@ -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 = {

View file

@ -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';

View file

@ -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';

View file

@ -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

View file

@ -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
}
`,
} }
}; };

View file

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

View file

@ -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;

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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',
]); ]);

View file

@ -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);

View file

@ -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',

View file

@ -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',
]); ]);

View file

@ -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);

View file

@ -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',

View file

@ -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',
]); ]);

View file

@ -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
} }

View file

@ -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',

View file

@ -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',
]); ]);

View file

@ -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';

View file

@ -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!

View file

@ -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';

View file

@ -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.

View file

@ -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;

View file

@ -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' });

View file

@ -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';

View file

@ -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,

View file

@ -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');

View file

@ -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}`),

View file

@ -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>

View 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 = {

View file

@ -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');

View file

@ -1,7 +1,7 @@
Package.describe({ Package.describe({
name: "vulcan:admin", name: "vulcan:admin",
summary: "Vulcan components package", summary: "Vulcan components package",
version: '1.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',
]); ]);

View file

@ -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");

View file

@ -12,7 +12,7 @@ import { addCallback, getActions } from 'meteor/vulcan:lib';
* @param {Object} Redux store reference instantiated on the current connected client * @param {Object} 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;

View file

@ -1,36 +1,68 @@
import { Components, registerComponent, registerSetting, getSetting, Strings } from 'meteor/vulcan:lib'; import {
Components,
registerComponent,
registerSetting,
getSetting,
Strings,
runCallbacks,
} from 'meteor/vulcan:lib';
import React, { PureComponent } from 'react'; import 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';

View file

@ -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}>

View file

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

View file

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

View file

@ -2,6 +2,7 @@ import React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; import 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>
); );
} }

View file

@ -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>

View file

@ -0,0 +1,20 @@
import { Components, registerComponent } from 'meteor/vulcan:lib';
import React from 'react';
import Button from 'react-bootstrap/lib/Button';
import { FormattedMessage, intlShape } from 'meteor/vulcan:i18n';
const 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);

View file

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

View file

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

View file

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

View file

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

View file

@ -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.`
});
}

View file

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

View file

@ -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';

View file

@ -1,7 +1,7 @@
Package.describe({ Package.describe({
name: "vulcan:core", name: "vulcan:core",
summary: "Vulcan core package", summary: "Vulcan core package",
version: '1.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');

View file

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

View file

@ -37,7 +37,7 @@ class Email extends PureComponent {
<td>{name}</td> <td>{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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,6 +4,7 @@ addRoute([
// {name: 'cheatsheet', path: '/cheatsheet', component: import('./components/Cheatsheet.jsx')}, // {name: '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'},
]); ]);

View file

@ -1,7 +1,7 @@
Package.describe({ Package.describe({
name: "vulcan:debug", name: "vulcan:debug",
summary: "Vulcan debug package", summary: "Vulcan debug package",
version: '1.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',
]); ]);

View file

@ -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 };
} }

View file

@ -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>

View file

@ -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");

View file

@ -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';

View file

@ -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'
]); ]);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,20 @@
Package.describe({
name: "vulcan:events-ga",
summary: "Vulcan Google Analytics event tracking package",
version: '1.8.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');
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,20 @@
Package.describe({
name: 'vulcan:events-intercom',
summary: 'Vulcan Intercom integration package.',
version: '1.8.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