Merge branch 'devel' into apollo2

This commit is contained in:
SachaG 2019-01-30 10:34:27 +09:00
commit 19ea8159b3
53 changed files with 923 additions and 492 deletions

View file

@ -27,8 +27,9 @@
] ]
} }
], ],
"babel/array-bracket-spacing": 0, "babel/array-bracket-spacing": 1,
"babel/object-curly-spacing": 0, "babel/object-curly-spacing": 0,
# "babel/object-curly-spacing": [1, "always", { "objectsInObjects": false, "arraysInObjects": false }],
"babel/object-shorthand": 0, "babel/object-shorthand": 0,
"babel/arrow-parens": 0, "babel/arrow-parens": 0,
"no-await-in-loop": 1, "no-await-in-loop": 1,
@ -78,4 +79,4 @@
"param": true, "param": true,
"returns": true "returns": true
} }
} }

View file

@ -3,11 +3,11 @@
const {esNextPaths} = require('./.vulcan/shared/pathsByLanguageVersion'); const {esNextPaths} = require('./.vulcan/shared/pathsByLanguageVersion');
module.exports = { module.exports = {
bracketSpacing: false, bracketSpacing: true,
singleQuote: true, singleQuote: true,
jsxBracketSameLine: true, jsxBracketSameLine: true,
trailingComma: 'es5', trailingComma: 'es5',
printWidth: 80, printWidth: 100,
parser: 'babylon', parser: 'babylon',
overrides: [ overrides: [
@ -18,4 +18,4 @@ module.exports = {
}, },
}, },
], ],
}; };

2
.vulcan/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
bkp
package.json

99
.vulcan/update_package.js Normal file
View file

@ -0,0 +1,99 @@
#!/usr/bin/env node
var fs = require('fs');
var mergePackages = require('@userfrosting/merge-package-dependencies');
var jsdiff = require('diff');
require('colors');
function diffPartReducer(accumulator, part) {
// green for additions, red for deletions
// grey for common parts
var color = part.added ? 'green' : (part.removed ? 'red' : 'grey');
return {
text: (accumulator.text || '') + part.value[color],
count: (accumulator.count || 0) + (part.added || part.removed ? 1 : 0),
};
}
// copied from sort-object-keys package
function sortObjectByKeyNameList(object, sortWith) {
var keys;
var sortFn;
if (typeof sortWith === 'function') {
sortFn = sortWith;
} else {
keys = sortWith;
}
return (keys || []).concat(Object.keys(object).sort(sortFn)).reduce(function(total, key) {
total[key] = object[key];
return total;
}, Object.create({}));
}
var appDirPath = './';
var vulcanDirPath = './.vulcan/';
if (!fs.existsSync(vulcanDirPath + 'package.json')) {
console.log('Could not find \'' + vulcanDirPath + 'package.json\'');
} else if (!fs.existsSync(appDirPath + 'package.json')) {
console.log('Could not find \'' + appDirPath + 'package.json\'');
} else {
var appPackageFile = fs.readFileSync(appDirPath + '/package.json');
var appPackageJson = JSON.parse(appPackageFile);
var vulcanPackageFile = fs.readFileSync(vulcanDirPath + 'package.json');
var vulcanPackageJson = JSON.parse(vulcanPackageFile);
if (appPackageJson.vulcanVersion) {
console.log(appPackageJson.name + '@' + appPackageJson.version +
' \'package.json\' will be updated from Vulcan@' + appPackageJson.vulcanVersion +
' to Vulcan@' + vulcanPackageJson.version +
' dependencies.');
} else {
console.log(appPackageJson.name + '@' + appPackageJson.version +
' \'package.json\' will be updated with Vulcan@' + vulcanPackageJson.version +
' dependencies.');
}
var backupDirPath = vulcanDirPath + 'bkp/';
if (!fs.existsSync(backupDirPath)) {
fs.mkdirSync(backupDirPath);
}
var backupFilePath = backupDirPath + 'package-' + Date.now() + '.json';
console.log('Saving a backup of \'' + appDirPath + 'package.json\' in \'' + backupFilePath + '\'');
fs.writeFileSync(backupFilePath, appPackageFile);
var updatedAppPackageJson = mergePackages.npm(
// IMPORTANT: parse again because mergePackages.npm mutates json
JSON.parse(appPackageFile),
[vulcanDirPath]
);
updatedAppPackageJson.vulcanVersion = vulcanPackageJson.version;
[
'dependencies',
'devDependencies',
'peerDependencies'
].forEach(function(key) {
if (updatedAppPackageJson[key]) {
updatedAppPackageJson[key] = sortObjectByKeyNameList(updatedAppPackageJson[key]);
}
const diff = jsdiff.diffJson(
sortObjectByKeyNameList(appPackageJson[key] || {}),
updatedAppPackageJson[key] || {}
).reduce(diffPartReducer, {});
if (diff.count) {
console.log('Changes in "' + key + '":');
console.log(diff.text);
} else {
console.log('No changes in "' + key + '".');
}
});
fs.writeFileSync(appDirPath + 'package.json', JSON.stringify(updatedAppPackageJson, null, ' '));
}

View file

@ -11,7 +11,8 @@
"test-unit": "TEST_WATCH=1 meteor test-packages ./packages/* --port 3002 --driver-package meteortesting:mocha --raw-logs", "test-unit": "TEST_WATCH=1 meteor test-packages ./packages/* --port 3002 --driver-package meteortesting:mocha --raw-logs",
"test": "npm run test-unit", "test": "npm run test-unit",
"prettier": "node ./.vulcan/prettier/index.js write-changed", "prettier": "node ./.vulcan/prettier/index.js write-changed",
"prettier-all": "node ./.vulcan/prettier/index.js write" "prettier-all": "node ./.vulcan/prettier/index.js write",
"update-package-json": "node ./.vulcan/update_package.js"
}, },
"husky": { "husky": {
"hooks": { "hooks": {
@ -105,10 +106,13 @@
}, },
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"@userfrosting/merge-package-dependencies": "^1.2.0",
"autoprefixer": "^6.3.6", "autoprefixer": "^6.3.6",
"babel-eslint": "^7.0.0", "babel-eslint": "^7.0.0",
"babylon": "^6.18.0", "babylon": "^6.18.0",
"colors": "^1.3.2",
"chromedriver": "^2.40.0", "chromedriver": "^2.40.0",
"diff": "^3.5.0",
"enzyme": "^3.3.0", "enzyme": "^3.3.0",
"enzyme-adapter-react-16.3": "^1.4.0", "enzyme-adapter-react-16.3": "^1.4.0",
"eslint": "^3.19.0", "eslint": "^3.19.0",

View file

@ -6,9 +6,9 @@ export class AccountsFormMessage extends React.Component {
let { message, type, className = 'message', style = {} } = this.props; let { message, type, className = 'message', style = {} } = this.props;
message = _.isObject(message) ? message.message : message; // If message is object, then try to get message from it message = _.isObject(message) ? message.message : message; // If message is object, then try to get message from it
return message ? ( return message ? (
<div style={style} className={[ className, type ].join(' ')}>{ message }</div> <div style={style} className={[className, type].join(' ')}>{ message }</div>
) : null; ) : null;
} }
} }
registerComponent('AccountsFormMessage', AccountsFormMessage); registerComponent('AccountsFormMessage', AccountsFormMessage);

View file

@ -0,0 +1,27 @@
/**
* @Author: Apollinaire Lecocq <apollinaire>
* @Date: 08-01-19
* @Last modified by: apollinaire
* @Last modified time: 10-01-19
*/
import React from 'react';
import {registerComponent, Components, withAccess, Dummy} from 'meteor/vulcan:core';
const RestrictToAdmins = withAccess({groups: ['admins']})(Dummy);
/**
* A simple component that renders the existing layout and checks wether the currentUser is an admin or not.
*/
function AdminLayout({children}) {
return (
<Components.Layout>
<RestrictToAdmins>{children}</RestrictToAdmins>
</Components.Layout>
);
}
registerComponent({
name: 'AdminLayout',
component: AdminLayout,
});

View file

@ -1,3 +1,4 @@
import './fragments.js'; import './fragments.js';
import './routes.js'; import './routes.js';
import './i18n.js'; import './i18n.js';
import '../components/AdminLayout';

View file

@ -1,5 +1,14 @@
import { addRoute, getDynamicComponent } from 'meteor/vulcan:core'; import {addRoute, getDynamicComponent} from 'meteor/vulcan:core';
import React from 'react'; import React from 'react';
addRoute({ name: 'admin', path: '/admin', component: () => getDynamicComponent(import('../components/AdminHome.jsx'))}); addRoute({
addRoute({ name: 'admin2', path: '/admin/users', component: () => getDynamicComponent(import('../components/AdminHome.jsx'))}); name: 'admin',
path: '/admin',
component: () => getDynamicComponent(import('../components/AdminHome.jsx')),
layoutName: 'AdminLayout',
});
addRoute({
name: 'admin2',
path: '/admin/users',
component: () => getDynamicComponent(import('../components/AdminHome.jsx')),
});

View file

@ -20,13 +20,11 @@ import moment from 'moment';
import { Switch, Route } from 'react-router-dom'; import { Switch, Route } from 'react-router-dom';
import { withRouter} from 'react-router'; import { withRouter} from 'react-router';
const DummyErrorCatcher = ({ children }) => children;
// see https://stackoverflow.com/questions/42862028/react-router-v4-with-multiple-layouts // see https://stackoverflow.com/questions/42862028/react-router-v4-with-multiple-layouts
const RouteWithLayout = ({ layoutName, component, currentRoute, ...rest }) => { const RouteWithLayout = ({ layoutName, component, currentRoute, ...rest }) => {
// if defined, use ErrorCatcher component to wrap layout contents // if defined, use ErrorCatcher component to wrap layout contents
const ErrorCatcher = Components.ErrorCatcher ? Components.ErrorCatcher : DummyErrorCatcher; const ErrorCatcher = Components.ErrorCatcher ? Components.ErrorCatcher : Components.Dummy;
return ( return (
<Route <Route
@ -38,8 +36,8 @@ const RouteWithLayout = ({ layoutName, component, currentRoute, ...rest }) => {
{...rest} {...rest}
render={props => { render={props => {
const layoutProps = {...props, currentRoute} const layoutProps = { ...props, currentRoute };
const childComponentProps = {...props, currentRoute} const childComponentProps = { ...props, currentRoute };
const layout = layoutName ? Components[layoutName] : Components.Layout; const layout = layoutName ? Components[layoutName] : Components.Layout;
return React.createElement(layout, layoutProps, <ErrorCatcher>{React.createElement(component, childComponentProps)}</ErrorCatcher>); return React.createElement(layout, layoutProps, <ErrorCatcher>{React.createElement(component, childComponentProps)}</ErrorCatcher>);
}} }}

View file

@ -187,4 +187,4 @@ Card.contextTypes = {
intl: intlShape intl: intlShape
}; };
registerComponent('Card', Card); registerComponent('Card', Card);

View file

@ -0,0 +1,10 @@
import React from 'react';
import {registerComponent} from 'meteor/vulcan:lib';
function Dummy({children}) {
return children;
}
Dummy.displayName = 'Dummy';
registerComponent({name: 'Dummy', component: Dummy});
export default Dummy;

View file

@ -47,7 +47,7 @@ class Flash extends PureComponent {
const flashType = type === 'error' ? 'danger' : type; // if flashType is "error", use "danger" instead const flashType = type === 'error' ? 'danger' : type; // if flashType is "error", use "danger" instead
return ( return (
<Components.Alert className="flash-message" variant={flashType} onDismiss={this.dismissFlash}> <Components.Alert className="flash-message" variant={flashType} onClose={this.dismissFlash}>
<span dangerouslySetInnerHTML={{ __html: message }} /> <span dangerouslySetInnerHTML={{ __html: message }} />
</Components.Alert> </Components.Alert>
); );

View file

@ -1,31 +1,63 @@
import React, { PureComponent } from 'react'; import React, {PureComponent} from 'react';
import { withCurrentUser } from 'meteor/vulcan:core'; import {Components} from 'meteor/vulcan:lib';
import { withRouter } from 'react-router'; import withCurrentUser from './withCurrentUser';
import {withRouter} from 'react-router';
import Users from 'meteor/vulcan:users'; import Users from 'meteor/vulcan:users';
export default function withAccess (options) { /**
* withAccess - description
*
* @param {Object} options the options that define the hoc
* @param {string[]} options.groups the groups that have access to this component
* @param {string} options.redirect the link to redirect to in case the access is not granted (optional)
* @param {string} options.failureComponentName the name of a component to display if access is not granted (optional)
* @param {Component} options.failureComponent the component to display if access is not granted (optional)
* @return {PureComponent} a React component that will display only if the acces is granted
*/
const { groups, redirect } = options; export default function withAccess(options) {
const {
groups = [],
redirect = null,
failureComponent = null,
failureComponentName = null,
} = options;
// we return a function that takes a component and itself returns a component // we return a function that takes a component and itself returns a component
return WrappedComponent => { return WrappedComponent => {
class AccessComponent extends PureComponent { class AccessComponent extends PureComponent {
// if there are any groups defined check if user belongs, else just check if user exists // if there are any groups defined check if user belongs, else just check if user exists
canAccess = currentUser => { canAccess = currentUser => {
return groups ? Users.isMemberOf(currentUser, groups) : currentUser; return groups ? Users.isMemberOf(currentUser, groups) : currentUser;
} };
// redirect on constructor if user cannot access // redirect on constructor if user cannot access
constructor(props) { constructor(props) {
super(props); super(props);
if(!this.canAccess(props.currentUser) && typeof redirect === 'string') { if (
!this.canAccess(props.currentUser) &&
typeof redirect === 'string'
) {
props.router.push(redirect); props.router.push(redirect);
} }
} }
renderFailureComponent() {
if (failureComponentName) {
const FailureComponent = Components[failureComponentName];
return <FailureComponent {...this.props} />;
} else if (failureComponent) {
const FailureComponent = failureComponent; // necesary because jsx components must be uppercase
return <FailureComponent {...this.props} />;
} else return null;
}
render() { render() {
return this.canAccess(this.props.currentUser) ? <WrappedComponent {...this.props}/> : null; return this.canAccess(this.props.currentUser) ? (
<WrappedComponent {...this.props} />
) : (
this.renderFailureComponent()
);
} }
} }

View file

@ -419,14 +419,12 @@ const registerCollectionCallbacks = (typeName, options) => {
if (options.create) { if (options.create) {
registerCallback({ registerCallback({
name: `${typeName}.create.validate`, name: `${typeName}.create.validate`,
iterator: {document: 'The document being inserted'}, iterator: { validationErrors: 'An array that can be used to accumulate validation errors' },
properties: [ properties: [
{document: 'The document being inserted'}, { document: 'The document being inserted' },
{currentUser: 'The current user'}, { currentUser: 'The current user' },
{ { collection: 'The collection the document belongs to' },
validationErrors: { context: 'The context of the mutation'},
'An object that can be used to accumulate validation errors',
},
], ],
runs: 'sync', runs: 'sync',
returns: 'document', returns: 'document',
@ -467,14 +465,13 @@ const registerCollectionCallbacks = (typeName, options) => {
if (options.update) { if (options.update) {
registerCallback({ registerCallback({
name: `${typeName}.update.validate`, name: `${typeName}.update.validate`,
iterator: {data: 'The client data'}, iterator: { validationErrors: 'An object that can be used to accumulate validation errors' },
properties: [ properties: [
{document: 'The document being edited'}, { document: 'The document being edited' },
{currentUser: 'The current user'}, { data: 'The client data' },
{ { currentUser: 'The current user' },
validationErrors: { collection: 'The collection the document belongs to' },
'An object that can be used to accumulate validation errors', { context: 'The context of the mutation'},
},
], ],
runs: 'sync', runs: 'sync',
returns: 'modifier', returns: 'modifier',
@ -522,13 +519,12 @@ const registerCollectionCallbacks = (typeName, options) => {
if (options.delete) { if (options.delete) {
registerCallback({ registerCallback({
name: `${typeName}.delete.validate`, name: `${typeName}.delete.validate`,
iterator: {document: 'The document being removed'}, iterator: { validationErrors: 'An object that can be used to accumulate validation errors' },
properties: [ properties: [
{currentUser: 'The current user'}, { currentUser: 'The current user' },
{ { document: 'The document being removed' },
validationErrors: { collection: 'The collection the document belongs to'},
'An object that can be used to accumulate validation errors', { context: 'The context of this mutation'}
},
], ],
runs: 'sync', runs: 'sync',
returns: 'document', returns: 'document',

View file

@ -25,6 +25,7 @@ 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 RouterHook } from './components/RouterHook.jsx';
export { default as ScrollToTop } from './components/ScrollToTop.jsx'; export { default as ScrollToTop } from './components/ScrollToTop.jsx';
export { default as Dummy } from './components/Dummy.jsx';
export { default as withAccess } from './containers/withAccess.js'; export { default as withAccess } from './containers/withAccess.js';
export { default as withMessages } from './containers/withMessages.js'; export { default as withMessages } from './containers/withMessages.js';
@ -36,6 +37,7 @@ export { default as withDelete } from './containers/withDelete.js';
export { default as withCurrentUser } from './containers/withCurrentUser.js'; export { default as withCurrentUser } from './containers/withCurrentUser.js';
export { default as withMutation } from './containers/withMutation.js'; export { default as withMutation } from './containers/withMutation.js';
export { default as withUpsert } from './containers/withUpsert.js'; export { default as withUpsert } from './containers/withUpsert.js';
export { default as withSiteData } from './containers/withSiteData.js';
export { default as withComponents } from './containers/withComponents'; export { default as withComponents } from './containers/withComponents';

View file

@ -1,10 +0,0 @@
import React from 'react';
import { Components, registerComponent } from 'meteor/vulcan:lib';
const adminStyles = {
padding: '20px'
};
const AdminLayout = props => <div className="admin-layout" style={adminStyles}>{props.children}</div>;
registerComponent('AdminLayout', AdminLayout);

View file

@ -0,0 +1,10 @@
import React from 'react';
import { Components, registerComponent } from 'meteor/vulcan:lib';
const debugStyles = {
padding: '20px'
};
const DebugLayout = props => <div className="debug-layout" style={debugStyles}>{props.children}</div>;
registerComponent('DebugLayout', DebugLayout);

View file

@ -2,25 +2,30 @@ import React from 'react';
import { registerComponent, Components, Routes } from 'meteor/vulcan:lib'; import { registerComponent, Components, Routes } from 'meteor/vulcan:lib';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
const RoutePath = ({ document }) => const RoutePath = ({document}) => (
<Link to={document.path}>{document.path}</Link>; <Link to={document.path}>{document.path}</Link>
);
const RoutesDashboard = props => const RoutesDashboard = props => {
<div className="routes"> return (
<Components.Datatable <div className="routes">
showSearch={false} <Components.Datatable
showNew={false} showSearch={false}
showEdit={false} showNew={false}
data={Object.values(Routes)} showEdit={false}
columns={[ data={Object.values(Routes)}
'name', columns={[
{ 'name',
name: 'path', {
component: RoutePath name: 'path',
}, component: RoutePath,
'componentName', },
]} 'componentName',
/> 'layoutName'
</div>; ]}
/>
</div>
);
};
registerComponent('Routes', RoutesDashboard); registerComponent('Routes', RoutesDashboard);

View file

@ -1,4 +1,4 @@
import '../components/AdminLayout.jsx'; import '../components/DebugLayout.jsx';
import '../components/Emails.jsx'; import '../components/Emails.jsx';
import '../components/Groups.jsx'; import '../components/Groups.jsx';

View file

@ -1,14 +1,54 @@
import { addRoute, getDynamicComponent } from 'meteor/vulcan:lib'; import {addRoute, getDynamicComponent} from 'meteor/vulcan:lib';
addRoute([ addRoute([
// {name: 'cheatsheet', path: '/cheatsheet', component: import('./components/Cheatsheet.jsx')}, // {name: 'cheatsheet', path: '/cheatsheet', component: import('./components/Cheatsheet.jsx')},
{ name: 'debug', path: '/debug', componentName: 'DebugDashboard', layoutName: 'AdminLayout' }, {
{ name: 'debugGroups', path: '/debug/groups', component: () => getDynamicComponent(import('../components/Groups.jsx')), layoutName: 'AdminLayout' }, name: 'debug',
{ name: 'debugSettings', path: '/debug/settings', componentName: 'Settings', layoutName: 'AdminLayout' }, path: '/debug',
{ name: 'debugCallbacks', path: '/debug/callbacks', componentName: 'Callbacks', layoutName: 'AdminLayout' }, componentName: 'DebugDashboard',
layoutName: 'DebugLayout',
},
{
name: 'debugGroups',
path: '/debug/groups',
component: () => getDynamicComponent(import('../components/Groups.jsx')),
layoutName: 'DebugLayout',
},
{
name: 'debugSettings',
path: '/debug/settings',
componentName: 'Settings',
layoutName: 'DebugLayout',
},
{
name: 'debugCallbacks',
path: '/debug/callbacks',
componentName: 'Callbacks',
layoutName: 'DebugLayout',
},
// {name: 'emails', path: '/emails', component: () => getDynamicComponent(import('./components/Emails.jsx'))}, // {name: 'emails', path: '/emails', component: () => getDynamicComponent(import('./components/Emails.jsx'))},
{ name: 'debugEmails', path: '/debug/emails', componentName: 'Emails', layoutName: 'AdminLayout' }, {
{ name: 'debugRoutes', path: '/debug/routes', componentName: 'Routes', layoutName: 'AdminLayout' }, name: 'debugEmails',
{ name: 'debugComponents', path: '/debug/components', componentName: 'Components', layoutName: 'AdminLayout' }, path: '/debug/emails',
{ name: 'debugI18n', path: '/debug/i18n', componentName: 'I18n', layoutName: 'AdminLayout' }, componentName: 'Emails',
layoutName: 'DebugLayout',
},
{
name: 'debugRoutes',
path: '/debug/routes',
componentName: 'Routes',
layoutName: 'DebugLayout',
},
{
name: 'debugComponents',
path: '/debug/components',
componentName: 'Components',
layoutName: 'DebugLayout',
},
{
name: 'debugI18n',
path: '/debug/i18n',
componentName: 'I18n',
layoutName: 'DebugLayout',
},
]); ]);

View file

@ -4,6 +4,7 @@ import Juice from 'juice';
import htmlToText from 'html-to-text'; import htmlToText from 'html-to-text';
import Handlebars from 'handlebars'; import Handlebars from 'handlebars';
import { Utils, getSetting, registerSetting, runQuery, Strings, getString } from 'meteor/vulcan:lib'; // import from vulcan:lib because vulcan:core is not loaded yet import { Utils, getSetting, registerSetting, runQuery, Strings, getString } from 'meteor/vulcan:lib'; // import from vulcan:lib because vulcan:core is not loaded yet
import { Email } from 'meteor/email';
/* /*
@ -82,13 +83,13 @@ VulcanEmail.generateTextVersion = html => {
}); });
}; };
VulcanEmail.send = (to, subject, html, text, throwErrors, cc, bcc, replyTo, headers) => { VulcanEmail.send = (to, subject, html, text, throwErrors, cc, bcc, replyTo, headers, attachments) => {
// TODO: limit who can send emails // TODO: limit who can send emails
// TODO: fix this error: Error: getaddrinfo ENOTFOUND // TODO: fix this error: Error: getaddrinfo ENOTFOUND
if (typeof to === 'object') { if (typeof to === 'object') {
// eslint-disable-next-line no-redeclare // eslint-disable-next-line no-redeclare
var { to, cc, bcc, replyTo, subject, html, text, throwErrors, headers } = to; var { to, cc, bcc, replyTo, subject, html, text, throwErrors, headers, attachments } = to;
} }
const from = getSetting('defaultEmail', 'noreply@example.com'); const from = getSetting('defaultEmail', 'noreply@example.com');
@ -102,14 +103,15 @@ VulcanEmail.send = (to, subject, html, text, throwErrors, cc, bcc, replyTo, head
const email = { const email = {
from: from, from: from,
to: to, to,
cc: cc, cc,
bcc: bcc, bcc,
replyTo: replyTo, replyTo,
subject: subject, subject,
headers: headers, headers,
text: text, text,
html: html, html,
attachments,
}; };
const shouldSendEmail = process.env.NODE_ENV === 'production' || getSetting('enableDevelopmentEmails', false); const shouldSendEmail = process.env.NODE_ENV === 'production' || getSetting('enableDevelopmentEmails', false);
@ -153,9 +155,9 @@ VulcanEmail.build = async ({ emailName, variables, locale }) => {
return { data, subject, html }; return { data, subject, html };
}; };
VulcanEmail.buildAndSend = async ({ to, cc, bcc, replyTo, emailName, variables, locale = getSetting('locale'), headers }) => { VulcanEmail.buildAndSend = async ({ to, cc, bcc, replyTo, emailName, variables, locale = getSetting('locale'), headers, attachments }) => {
const email = await VulcanEmail.build({ to, emailName, variables, locale }); const email = await VulcanEmail.build({ to, emailName, variables, locale });
return VulcanEmail.send({ to, cc, bcc, replyTo, subject: email.subject, html: email.html, headers }); return VulcanEmail.send({ to, cc, bcc, replyTo, subject: email.subject, html: email.html, headers, attachments });
}; };
VulcanEmail.buildAndSendHTML = (to, subject, html) => VulcanEmail.send(to, subject, VulcanEmail.buildTemplate(html)); VulcanEmail.buildAndSendHTML = (to, subject, html) => VulcanEmail.send(to, subject, VulcanEmail.buildTemplate(html));

View file

@ -3,9 +3,7 @@ import { withMutation } from 'meteor/vulcan:core';
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; 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 Form from 'react-bootstrap/lib/Form';
const Input = FRC.Input;
class EmbedURL extends Component { class EmbedURL extends Component {
constructor(props) { constructor(props) {
@ -161,7 +159,7 @@ class EmbedURL extends Component {
<label className="control-label col-sm-3">{this.props.label}</label> <label className="control-label col-sm-3">{this.props.label}</label>
<div className="col-sm-9 embedly-form-control"> <div className="col-sm-9 embedly-form-control">
<div className="embedly-url-field"> <div className="embedly-url-field">
<Input <Form.Control
{...inputProperties} {...inputProperties}
onBlur={this.handleBlur} onBlur={this.handleBlur}
type="url" type="url"

View file

@ -10,7 +10,8 @@ Usage:
*/ */
import { Components, registerComponent, withCurrentUser } from 'meteor/vulcan:core'; import { Components, registerComponent, withCurrentUser, withSiteData } from 'meteor/vulcan:core';
import { withRouter } from 'react-router';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Errors } from '../modules/errors.js'; import { Errors } from '../modules/errors.js';
@ -31,11 +32,26 @@ class ErrorCatcher extends Component {
}); });
}; };
componentDidUpdate(prevProps) {
if (
this.props.location &&
prevProps.location &&
this.props.location.pathname &&
prevProps.location.pathname &&
prevProps.location.pathname !== this.props.location.pathname
) {
// reset the component state when the route changes to re-render the app and avodi blocking the navigation
this.setState({ error: null });
}
}
render() { render() {
const { error } = this.state; const { error } = this.state;
return error ? ( return error ? (
<div className="error-catcher"> <div className="error-catcher">
<Components.Flash message={{ id: 'errors.generic_report', properties: { errorMessage: error.message } }} /> <Components.Flash
message={{ id: 'errors.generic_report', properties: { errorMessage: error.message } }}
/>
</div> </div>
) : ( ) : (
this.props.children this.props.children
@ -43,4 +59,4 @@ class ErrorCatcher extends Component {
} }
} }
registerComponent('ErrorCatcher', ErrorCatcher, withCurrentUser); registerComponent('ErrorCatcher', ErrorCatcher, withCurrentUser, withSiteData, withRouter);

View file

@ -36,7 +36,6 @@ import React, { Component } from 'react';
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { intlShape } from 'meteor/vulcan:i18n'; import { intlShape } from 'meteor/vulcan:i18n';
import Formsy from 'formsy-react';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import get from 'lodash/get'; import get from 'lodash/get';
import set from 'lodash/set'; import set from 'lodash/set';
@ -319,13 +318,15 @@ class SmartForm extends Component {
Note: when submitting the form (getData()), do not include any extra fields. Note: when submitting the form (getData()), do not include any extra fields.
*/ */
getFieldNames = (args = {}) => { getFieldNames = (args) => {
// we do this to avoid having default values in arrow functions, which breaks MS Edge support. See https://github.com/meteor/meteor/issues/10171
let args0 = args || {};
const { const {
schema = this.state.schema, schema = this.state.schema,
excludeHiddenFields = true, excludeHiddenFields = true,
replaceIntlFields = false, replaceIntlFields = false,
addExtraFields = true addExtraFields = true
} = args; } = args0;
const { fields, addFields } = this.props; const { fields, addFields } = this.props;
@ -619,7 +620,7 @@ class SmartForm extends Component {
...newValues ...newValues
} // Submit form after setState update completed } // Submit form after setState update completed
}), }),
() => this.submitForm(this.form.getModel()) () => this.submitForm()
); );
}; };
@ -856,7 +857,7 @@ class SmartForm extends Component {
*/ */
formKeyDown = event => { formKeyDown = event => {
if ((event.ctrlKey || event.metaKey) && event.keyCode === 13) { if ((event.ctrlKey || event.metaKey) && event.keyCode === 13) {
this.submitForm(this.form.getModel()); this.submitForm();
} }
}; };
@ -920,9 +921,10 @@ class SmartForm extends Component {
Submit form handler Submit form handler
*/ */
submitForm = data => { submitForm = event => {
// note: we can discard the data collected by Formsy because all the data we need is already available via getDocument()
event && event.preventDefault();
// if form is disabled (there is already a submit handler running) don't do anything // if form is disabled (there is already a submit handler running) don't do anything
if (this.state.disabled) { if (this.state.disabled) {
return; return;
@ -931,9 +933,9 @@ class SmartForm extends Component {
// clear errors and disable form while it's submitting // clear errors and disable form while it's submitting
this.setState(prevState => ({ errors: [], disabled: true })); this.setState(prevState => ({ errors: [], disabled: true }));
// complete the data with values from custom components which are not being catched by Formsy mixin // complete the data with values from custom components
// note: it follows the same logic as SmartForm's getDocument method // note: it follows the same logic as SmartForm's getDocument method
data = this.getData({ replaceIntlFields: true, addExtraFields: false }); let data = this.getData({ replaceIntlFields: true, addExtraFields: false });
// if there's a submit callback, run it // if there's a submit callback, run it
if (this.props.submitCallback) { if (this.props.submitCallback) {
@ -988,67 +990,78 @@ class SmartForm extends Component {
} }
}; };
// --------------------------------------------------------------------- //
// ------------------------- Props to Pass ----------------------------- //
// --------------------------------------------------------------------- //
getFormProps = () => ({
className: 'document-' + this.getFormType(),
id: this.props.id,
onSubmit: this.submitForm,
onKeyDown: this.formKeyDown,
ref: e => {
this.form = e;
},
});
getFormErrorsProps = () => ({
errors: this.state.errors
});
getFormGroupProps = group => ({
key: group.name,
...group,
errors: this.state.errors,
throwError: this.throwError,
currentValues: this.state.currentValues,
updateCurrentValues: this.updateCurrentValues,
deletedValues: this.state.deletedValues,
addToDeletedValues: this.addToDeletedValues,
clearFieldErrors: this.clearFieldErrors,
formType: this.getFormType(),
currentUser: this.props.currentUser,
disabled: this.state.disabled,
formComponents: mergeWithComponents(this.props.formComponents),
});
getFormSubmitProps = () => ({
submitLabel: this.props.submitLabel,
cancelLabel: this.props.cancelLabel,
revertLabel: this.props.revertLabel,
cancelCallback: this.props.cancelCallback,
revertCallback: this.props.revertCallback,
document: this.getDocument(),
deleteDocument:
(this.getFormType() === 'edit' &&
this.props.showRemove &&
this.deleteDocument) ||
null,
collectionName:this.props.collectionName,
currentValues:this.state.currentValues,
deletedValues:this.state.deletedValues,
errors:this.state.errors,
});
// --------------------------------------------------------------------- // // --------------------------------------------------------------------- //
// ----------------------------- Render -------------------------------- // // ----------------------------- Render -------------------------------- //
// --------------------------------------------------------------------- // // --------------------------------------------------------------------- //
render() { render() {
const fieldGroups = this.getFieldGroups();
const collectionName = this.props.collectionName;
const FormComponents = mergeWithComponents(this.props.formComponents); const FormComponents = mergeWithComponents(this.props.formComponents);
return ( return (
<div className={'document-' + this.getFormType()}> <FormComponents.FormElement {...this.getFormProps()}>
<Formsy.Form <FormComponents.FormErrors {...this.getFormErrorsProps()} />
id={this.props.id}
onSubmit={this.submitForm}
onKeyDown={this.formKeyDown}
ref={e => {
this.form = e;
}}
>
<FormComponents.FormErrors errors={this.state.errors} />
{fieldGroups.map(group => ( {this.getFieldGroups().map(group => (
<FormComponents.FormGroup <FormComponents.FormGroup {...this.getFormGroupProps(group)} />
key={group.name} ))}
{...group}
errors={this.state.errors}
throwError={this.throwError}
currentValues={this.state.currentValues}
updateCurrentValues={this.updateCurrentValues}
deletedValues={this.state.deletedValues}
addToDeletedValues={this.addToDeletedValues}
clearFieldErrors={this.clearFieldErrors}
formType={this.getFormType()}
currentUser={this.props.currentUser}
disabled={this.state.disabled}
formComponents={FormComponents}
/>
))}
{this.props.repeatErrors && this.renderErrors()} {this.props.repeatErrors && this.renderErrors()}
<FormComponents.FormSubmit <FormComponents.FormSubmit {...this.getFormSubmitProps()} />
submitLabel={this.props.submitLabel} </FormComponents.FormElement>
cancelLabel={this.props.cancelLabel}
revertLabel={this.props.revertLabel}
cancelCallback={this.props.cancelCallback}
revertCallback={this.props.revertCallback}
document={this.getDocument()}
deleteDocument={
(this.getFormType() === 'edit' &&
this.props.showRemove &&
this.deleteDocument) ||
null
}
collectionName={collectionName}
currentValues={this.state.currentValues}
deletedValues={this.state.deletedValues}
errors={this.state.errors}
/>
</Formsy.Form>
</div>
); );
} }
} }

View file

@ -28,14 +28,14 @@ class FormComponent extends Component {
} }
const { currentValues, deletedValues, errors } = nextProps; const { currentValues, deletedValues, errors } = nextProps;
const { path } = this.props; const path = this.getPath(this.props);
// when checking for deleted values, both current path ('foo') and child path ('foo.0.bar') should trigger updates // when checking for deleted values, both current path ('foo') and child path ('foo.0.bar') should trigger updates
const includesPathOrChildren = deletedValues => const includesPathOrChildren = deletedValues =>
deletedValues.some(deletedPath => deletedPath.includes(path)); deletedValues.some(deletedPath => deletedPath.includes(path));
const valueChanged = const valueChanged =
get(currentValues, path) !== get(this.props.currentValues, path); !isEqual(get(currentValues, path), get(this.props.currentValues, path));
const errorChanged = !isEqual(this.getErrors(errors), this.getErrors()); const errorChanged = !isEqual(this.getErrors(errors), this.getErrors());
const deleteChanged = const deleteChanged =
includesPathOrChildren(deletedValues) !== includesPathOrChildren(deletedValues) !==
@ -93,7 +93,8 @@ class FormComponent extends Component {
Function passed to form controls (always controlled) to update their value Function passed to form controls (always controlled) to update their value
*/ */
handleChange = (name, value) => { handleChange = value => {
// if value is an empty string, delete the field // if value is an empty string, delete the field
if (value === '') { if (value === '') {
value = null; value = null;

View file

@ -97,15 +97,12 @@ class FormNestedArray extends PureComponent {
) )
), ),
(!maxCount || arrayLength < maxCount) && ( (!maxCount || arrayLength < maxCount) && (
<Components.Button <Components.FormNestedFoot
key="add-button" key="add-button"
size="small" addItem={this.addItem}
variant="success" label={this.props.label}
onClick={this.addItem} className="form-nested-foot"
className="form-nested-button" />
>
<Components.IconAdd height={12} width={12} />
</Components.Button>
), ),
hasErrors ? ( hasErrors ? (
<FormComponents.FieldErrors <FormComponents.FieldErrors

View file

@ -2,8 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Components, registerComponent } from 'meteor/vulcan:core'; import { Components, registerComponent } from 'meteor/vulcan:core';
const FormNestedFoot = ({ label, addItem }) => ( const FormNestedFoot = ({ addItem }) => (
<Components.Button size="small" variant="success" iconButton onClick={addItem} className="form-nested-button"> <Components.Button size="small" variant="success" onClick={addItem} className="form-nested-button">
<Components.IconAdd height={12} width={12} /> <Components.IconAdd height={12} width={12} />
</Components.Button> </Components.Button>
); );

View file

@ -51,6 +51,8 @@ import {
import withCollectionProps from './withCollectionProps'; import withCollectionProps from './withCollectionProps';
import { callbackProps } from './propTypes'; import { callbackProps } from './propTypes';
const intlSuffix = '_intl';
class FormWrapper extends PureComponent { class FormWrapper extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
@ -94,8 +96,11 @@ class FormWrapper extends PureComponent {
// if "fields" prop is specified, restrict list of fields to it // if "fields" prop is specified, restrict list of fields to it
if (typeof fields !== 'undefined' && fields.length > 0) { if (typeof fields !== 'undefined' && fields.length > 0) {
queryFields = _.intersection(queryFields, fields); // add "_intl" suffix to all fields in case some of them are intl fields
mutationFields = _.intersection(mutationFields, fields); const fieldsWithIntlSuffix = fields.map(field => `${field}${intlSuffix}`);
const allFields = [...fields, ...fieldsWithIntlSuffix];
queryFields = _.intersection(queryFields, allFields);
mutationFields = _.intersection(mutationFields, allFields);
} }
// add "addFields" prop contents to list of fields // add "addFields" prop contents to list of fields
@ -105,7 +110,7 @@ class FormWrapper extends PureComponent {
} }
const convertFields = field => { const convertFields = field => {
return field.slice(-5) === '_intl' ? `${field}{ locale value }` : field; return field.slice(-5) === intlSuffix ? `${field}{ locale value }` : field;
}; };
// generate query fragment based on the fields that can be edited. Note: always add _id. // generate query fragment based on the fields that can be edited. Note: always add _id.

View file

@ -49,15 +49,23 @@ export function registerComponent(name, rawComponent, ...hocs) {
* @param {String} name The name of the component to get. * @param {String} name The name of the component to get.
* @returns {Function|React Component} A (wrapped) React component * @returns {Function|React Component} A (wrapped) React component
*/ */
export const getComponent = (name) => { export const getComponent = name => {
const component = ComponentsTable[name]; const component = ComponentsTable[name];
if (!component) { if (!component) {
throw new Error(`Component ${name} not registered.`); throw new Error(`Component ${name} not registered.`);
} }
if (component.hocs) { if (component.hocs) {
const hocs = component.hocs.map(hoc => { const hocs = component.hocs.map(hoc => {
if(!Array.isArray(hoc)) return hoc; if (!Array.isArray(hoc)) {
if (typeof hoc !== 'function') {
throw new Error(`In registered component ${name}, an hoc is of type ${typeof hoc}`);
}
return hoc;
}
const [actualHoc, ...args] = hoc; const [actualHoc, ...args] = hoc;
if (typeof actualHoc !== 'function') {
throw new Error(`In registered component ${name}, an hoc is of type ${typeof actualHoc}`);
}
return actualHoc(...args); return actualHoc(...args);
}); });
return compose(...hocs)(component.rawComponent); return compose(...hocs)(component.rawComponent);
@ -202,4 +210,3 @@ export const delayedComponent = name => {
// return proxy; // return proxy;
//}; //};
export const mergeWithComponents = myComponents => (myComponents ? { ...Components, ...myComponents } : Components); export const mergeWithComponents = myComponents => (myComponents ? { ...Components, ...myComponents } : Components);

View file

@ -13,7 +13,7 @@ import { delayedComponent } from './components';
* `Components.DynamicLoading` in the meantime. * `Components.DynamicLoading` in the meantime.
* *
* @example Register a component with a dynamic import * @example Register a component with a dynamic import
* registerComponent('MyComponent', dynamicComponent(() => import('./path/to/MyComponent'))); * registerComponent('MyComponent', dynamicLoader(() => import('./path/to/MyComponent')));
* *
* @example Pass a dynamic component to a route * @example Pass a dynamic component to a route
* import { addRoute, dynamicLoader, getDynamicComponent } from 'meteor/vulcan:core'; * import { addRoute, dynamicLoader, getDynamicComponent } from 'meteor/vulcan:core';

View file

@ -203,7 +203,10 @@ const createApolloServer = (givenOptions = {}, givenConfig = {}) => {
const headers = req.renderContext.originalHeaders || req.headers; const headers = req.renderContext.originalHeaders || req.headers;
options.context.locale = getHeaderLocale(headers, user && user.locale); options.context.locale = getHeaderLocale(headers, user && user.locale);
if (headers.apikey && (headers.apikey === getSetting('vulcan.apiKey'))) {
options.context.currentUser = { isAdmin: true, isApiUser: true };
}
// console.log('// apollo_server.js isSSR?', !!req.renderContext.originalHeaders ? 'yes' : 'no'); // console.log('// apollo_server.js isSSR?', !!req.renderContext.originalHeaders ? 'yes' : 'no');
// console.log('// apollo_server.js headers:'); // console.log('// apollo_server.js headers:');
// console.log(headers); // console.log(headers);

View file

@ -51,7 +51,7 @@ GraphQL @intl directive resolver
*/ */
class IntlDirective extends SchemaDirectiveVisitor { class IntlDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field, details) { visitFieldDefinition(field, details) {
const {resolve = defaultFieldResolver, name} = field; const { resolve = defaultFieldResolver, name } = field;
field.resolve = async function(...args) { field.resolve = async function(...args) {
const [doc, graphQLArguments, context] = args; const [doc, graphQLArguments, context] = args;
const fieldValue = await resolve.apply(this, args); const fieldValue = await resolve.apply(this, args);

View file

@ -1,25 +1,29 @@
/* /*
Mutations have four steps: Mutations have five steps:
1. Validation 1. Validation
If the mutation call is not trusted (i.e. it comes from a GraphQL mutation), If the mutator call is not trusted (for example, it comes from a GraphQL mutation),
we'll run all validate steps: we'll run all validate steps:
- Check that the current user has permission to insert/edit each field. - Check that the current user has permission to insert/edit each field.
- Add userId to document (insert only). - Add userId to document (insert only).
- Run validation callbacks. - Run validation callbacks.
2. Sync Callbacks 2. Before Callbacks
The second step is to run the mutation argument through all the sync callbacks. The second step is to run the mutation argument through all the [before] callbacks.
3. Operation 3. Operation
We then perform the insert/update/remove operation. We then perform the insert/update/remove operation.
4. Async Callbacks 4. After Callbacks
We then run the mutation argument through all the [after] callbacks.
5. Async Callbacks
Finally, *after* the operation is performed, we execute any async callbacks. Finally, *after* the operation is performed, we execute any async callbacks.
Being async, they won't hold up the mutation and slow down its response time Being async, they won't hold up the mutation and slow down its response time
@ -39,49 +43,63 @@ import isEmpty from 'lodash/isEmpty';
registerSetting('database', 'mongo', 'Which database to use for your back-end'); registerSetting('database', 'mongo', 'Which database to use for your back-end');
/*
Create
*/
export const createMutator = async ({ collection, document, data, currentUser, validate, context }) => { export const createMutator = async ({ collection, document, data, currentUser, validate, context }) => {
const { collectionName, typeName } = collection.options; // OpenCRUD backwards compatibility: accept either data or document
debug('');
debugGroup(`--------------- start \x1b[36m${collectionName} Create Mutator\x1b[0m ---------------`);
debug(`validate: ${validate}`);
debug(document || data);
// we don't want to modify the original document // we don't want to modify the original document
let newDocument = Object.assign({}, document || data); document = data || document;
const { collectionName, typeName } = collection.options;
const schema = collection.simpleSchema()._schema; const schema = collection.simpleSchema()._schema;
const callbackProperties = { currentUser, collection, context }; startDebugMutator(collectionName, 'Update', { validate, document });
/*
Properties
*/
const properties = { data, currentUser, collection, context, document: document };
/*
Validation
*/
if (validate) { if (validate) {
let validationErrors = []; let validationErrors = [];
validationErrors = validationErrors.concat(validateDocument(document, collection, context));
validationErrors = validationErrors.concat(validateDocument(newDocument, collection, context));
// run validation callbacks // run validation callbacks
validationErrors = await runCallbacks({ name: `${typeName.toLowerCase()}.create.validate`, iterator: validationErrors, properties: { ...callbackProperties, document: newDocument } }); validationErrors = await runCallbacks({ name: `${typeName.toLowerCase()}.create.validate`, iterator: validationErrors, properties });
validationErrors = await runCallbacks({ name: '*.create.validate', iterator: validationErrors, properties: { ...callbackProperties, document: newDocument } }); validationErrors = await runCallbacks({ name: '*.create.validate', iterator: validationErrors, properties });
// OpenCRUD backwards compatibility // OpenCRUD backwards compatibility
newDocument = await runCallbacks(`${collectionName.toLowerCase()}.new.validate`, newDocument, currentUser, validationErrors); document = await runCallbacks(`${collectionName.toLowerCase()}.new.validate`, document, currentUser, validationErrors);
if (validationErrors.length) { if (validationErrors.length) {
throwError({ id: 'app.validation_error', data: {break: true, errors: validationErrors}}); console.log(validationErrors); // eslint-disable-line no-console
throwError({ id: 'app.validation_error', data: { break: true, errors: validationErrors } });
} }
} }
// if user is logged in, check if userId field is in the schema and add it to document if needed /*
userId
If user is logged in, check if userId field is in the schema and add it to document if needed
*/
if (currentUser) { if (currentUser) {
const userIdInSchema = Object.keys(schema).find(key => key === 'userId'); const userIdInSchema = Object.keys(schema).find(key => key === 'userId');
if (!!userIdInSchema && !newDocument.userId) newDocument.userId = currentUser._id; if (!!userIdInSchema && !document.userId) document.userId = currentUser._id;
} }
/* /*
run onCreate step onCreate
note: cannot use forEach with async/await. note: cannot use forEach with async/await.
See https://stackoverflow.com/a/37576787/649299 See https://stackoverflow.com/a/37576787/649299
@ -89,19 +107,17 @@ export const createMutator = async ({ collection, document, data, currentUser, v
note: clone arguments in case callbacks modify them note: clone arguments in case callbacks modify them
*/ */
for(let fieldName of Object.keys(schema)) { for (let fieldName of Object.keys(schema)) {
let autoValue; let autoValue;
if (schema[fieldName].onCreate) { if (schema[fieldName].onCreate) {
// OpenCRUD backwards compatibility: keep both newDocument and data for now, but phase our newDocument eventually // OpenCRUD backwards compatibility: keep both newDocument and data for now, but phase out newDocument eventually
// eslint-disable-next-line no-await-in-loop autoValue = await schema[fieldName].onCreate(properties); // eslint-disable-line no-await-in-loop
autoValue = await schema[fieldName].onCreate({ newDocument: clone(newDocument), data: clone(newDocument), currentUser, context });
} else if (schema[fieldName].onInsert) { } else if (schema[fieldName].onInsert) {
// OpenCRUD backwards compatibility // OpenCRUD backwards compatibility
// eslint-disable-next-line no-await-in-loop autoValue = await schema[fieldName].onInsert(clone(document), currentUser); // eslint-disable-line no-await-in-loop
autoValue = await schema[fieldName].onInsert(clone(newDocument), currentUser);
} }
if (typeof autoValue !== 'undefined') { if (typeof autoValue !== 'undefined') {
newDocument[fieldName] = autoValue; document[fieldName] = autoValue;
} }
} }
@ -111,137 +127,164 @@ export const createMutator = async ({ collection, document, data, currentUser, v
// post.userAgent = this.connection.httpHeaders['user-agent']; // post.userAgent = this.connection.httpHeaders['user-agent'];
// } // }
// run sync callbacks /*
newDocument = await runCallbacks({ name: `${typeName.toLowerCase()}.create.before`, iterator: newDocument, properties: callbackProperties });
newDocument = await runCallbacks({ name: '*.create.before', iterator: newDocument, properties: callbackProperties }); Before
*/
document = await runCallbacks({ name: `${typeName.toLowerCase()}.create.before`, iterator: document, properties });
document = await runCallbacks({ name: '*.create.before', iterator: document, properties });
// OpenCRUD backwards compatibility // OpenCRUD backwards compatibility
newDocument = await runCallbacks(`${collectionName.toLowerCase()}.new.before`, newDocument, currentUser); document = await runCallbacks(`${collectionName.toLowerCase()}.new.before`, document, currentUser);
newDocument = await runCallbacks(`${collectionName.toLowerCase()}.new.sync`, newDocument, currentUser); document = await runCallbacks(`${collectionName.toLowerCase()}.new.sync`, document, currentUser);
// add _id to document /*
newDocument._id = await Connectors.create(collection, newDocument);
DB Operation
*/
document._id = await Connectors.create(collection, document);
/*
After
*/
// run any post-operation sync callbacks // run any post-operation sync callbacks
newDocument = await runCallbacks({ name: `${typeName.toLowerCase()}.create.after`, iterator: newDocument, properties: callbackProperties }); document = await runCallbacks({ name: `${typeName.toLowerCase()}.create.after`, iterator: document, properties });
newDocument = await runCallbacks({ name: '*.create.after', iterator: newDocument, properties: callbackProperties }); document = await runCallbacks({ name: '*.create.after', iterator: document, properties });
// OpenCRUD backwards compatibility // OpenCRUD backwards compatibility
newDocument = await runCallbacks(`${collectionName.toLowerCase()}.new.after`, newDocument, currentUser); document = await runCallbacks(`${collectionName.toLowerCase()}.new.after`, document, currentUser);
// get fresh copy of document from db
// TODO: not needed?
const insertedDocument = await Connectors.get(collection, newDocument._id);
// run async callbacks
// note: query for document to get fresh document with collection-hooks effects applied // note: query for document to get fresh document with collection-hooks effects applied
await runCallbacksAsync({ name: `${typeName.toLowerCase()}.create.async`, properties: { insertedDocument, document: insertedDocument, ...callbackProperties }}); document = await Connectors.get(collection, document._id);
await runCallbacksAsync({ name: '*.create.async', properties: { insertedDocument, document: insertedDocument, ...callbackProperties }});
/*
Async
*/
// note: make sure properties.document is up to date
await runCallbacksAsync({ name: `${typeName.toLowerCase()}.create.async`, properties: { ...properties, document: document } });
await runCallbacksAsync({ name: '*.create.async', properties });
// OpenCRUD backwards compatibility // OpenCRUD backwards compatibility
await runCallbacksAsync(`${collectionName.toLowerCase()}.new.async`, insertedDocument, currentUser, collection); await runCallbacksAsync(`${collectionName.toLowerCase()}.new.async`, document, currentUser, collection);
debug('\x1b[33m=> created new document: \x1b[0m'); endDebugMutator(collectionName, 'Create', { document });
debug(newDocument);
debugGroupEnd();
debug(`--------------- end \x1b[36m${collectionName} Create Mutator\x1b[0m ---------------`);
debug('');
return { data: newDocument }; return { data: document };
}; };
/*
export const updateMutator = async ({ collection, documentId, selector, data, set = {}, unset = {}, currentUser, validate, context, document }) => { Update
*/
export const updateMutator = async ({ collection, documentId, selector, data, set = {}, unset = {}, currentUser, validate, context, document: oldDocument }) => {
const { collectionName, typeName } = collection.options; const { collectionName, typeName } = collection.options;
const schema = collection.simpleSchema()._schema; const schema = collection.simpleSchema()._schema;
// OpenCRUD backwards compatibility // OpenCRUD backwards compatibility
selector = selector || { _id: documentId }; selector = selector || { _id: documentId };
data = data || modifierToData({ $set: set, $unset: unset }); data = data || modifierToData({ $set: set, $unset: unset });
startDebugMutator(collectionName, 'Update', { selector, data });
if (isEmpty(selector)) { if (isEmpty(selector)) {
throw new Error('Selector cannot be empty'); throw new Error('Selector cannot be empty');
} }
// get original document from database or arguments // get original document from database or arguments
document = document || await Connectors.get(collection, selector); oldDocument = oldDocument || (await Connectors.get(collection, selector));
if (!document) { if (!oldDocument) {
throw new Error(`Could not find document to update for selector: ${JSON.stringify(selector)}`); throw new Error(`Could not find document to update for selector: ${JSON.stringify(selector)}`);
} }
const callbackProperties = { data, document, currentUser, collection, context };
debug(''); // get a "preview" of the new document
debugGroup(`--------------- start \x1b[36m${collectionName} Update Mutator\x1b[0m ---------------`); let document = { ...oldDocument, ...data };
debug('// collectionName: ', collectionName); document = pickBy(document, f => f !== null);
debug('// selector: ', selector);
debug('// data: ', data);
/*
Properties
*/
const properties = { data, oldDocument, document, currentUser, collection, context };
/*
Validation
*/
if (validate) { if (validate) {
let validationErrors = []; let validationErrors = [];
validationErrors = validationErrors.concat(validateData(data, document, collection, context)); validationErrors = validationErrors.concat(validateData(data, document, collection, context));
validationErrors = await runCallbacks({ name: `${typeName.toLowerCase()}.update.validate`, iterator: validationErrors, properties: callbackProperties }); validationErrors = await runCallbacks({ name: `${typeName.toLowerCase()}.update.validate`, iterator: validationErrors, properties });
validationErrors = await runCallbacks({ name: '*.update.validate', iterator: validationErrors, properties: callbackProperties }); validationErrors = await runCallbacks({ name: '*.update.validate', iterator: validationErrors, properties });
// OpenCRUD backwards compatibility // OpenCRUD backwards compatibility
data = modifierToData(await runCallbacks(`${collectionName.toLowerCase()}.edit.validate`, dataToModifier(data), document, currentUser, validationErrors)); data = modifierToData(await runCallbacks(`${collectionName.toLowerCase()}.edit.validate`, dataToModifier(data), document, currentUser, validationErrors));
if (validationErrors.length) { if (validationErrors.length) {
// eslint-disable-next-line no-console console.log(validationErrors); // eslint-disable-line no-console
console.log('// validationErrors'); throwError({ id: 'app.validation_error', data: { break: true, errors: validationErrors } });
// eslint-disable-next-line no-console
console.log(validationErrors);
throwError({ id: 'app.validation_error', data: {break: true, errors: validationErrors}});
} }
} }
// get a "preview" of the new document /*
let newDocument = { ...document, ...data};
newDocument = pickBy(newDocument, f => f !== null);
// run onUpdate step onUpdate
for(let fieldName of Object.keys(schema)) {
*/
for (let fieldName of Object.keys(schema)) {
let autoValue; let autoValue;
if (schema[fieldName].onUpdate) { if (schema[fieldName].onUpdate) {
// eslint-disable-next-line no-await-in-loop autoValue = await schema[fieldName].onUpdate(properties); // eslint-disable-line no-await-in-loop
autoValue = await schema[fieldName].onUpdate({ data: clone(data), document, currentUser, newDocument, context });
} else if (schema[fieldName].onEdit) { } else if (schema[fieldName].onEdit) {
// OpenCRUD backwards compatibility // OpenCRUD backwards compatibility
// eslint-disable-next-line no-await-in-loop autoValue = await schema[fieldName].onEdit(dataToModifier(clone(data)), document, currentUser, document); // eslint-disable-line no-await-in-loop
autoValue = await schema[fieldName].onEdit(dataToModifier(clone(data)), document, currentUser, newDocument);
} }
if (typeof autoValue !== 'undefined') { if (typeof autoValue !== 'undefined') {
data[fieldName] = autoValue; data[fieldName] = autoValue;
} }
} }
// run sync callbacks /*
data = await runCallbacks({ name: `${typeName.toLowerCase()}.update.before`, iterator: data, properties: { newDocument, ...callbackProperties }});
data = await runCallbacks({ name: '*.update.before', iterator: data, properties: { newDocument, ...callbackProperties }}); Before
*/
data = await runCallbacks({ name: `${typeName.toLowerCase()}.update.before`, iterator: data, properties });
data = await runCallbacks({ name: '*.update.before', iterator: data, properties });
// OpenCRUD backwards compatibility // OpenCRUD backwards compatibility
data = modifierToData(await runCallbacks(`${collectionName.toLowerCase()}.edit.before`, dataToModifier(data), document, currentUser, newDocument)); data = modifierToData(await runCallbacks(`${collectionName.toLowerCase()}.edit.before`, dataToModifier(data), document, currentUser, document));
data = modifierToData(await runCallbacks(`${collectionName.toLowerCase()}.edit.sync`, dataToModifier(data), document, currentUser, newDocument)); data = modifierToData(await runCallbacks(`${collectionName.toLowerCase()}.edit.sync`, dataToModifier(data), document, currentUser, document));
// update connector requires a modifier, so get it from data // update connector requires a modifier, so get it from data
const modifier = dataToModifier(data); const modifier = dataToModifier(data);
// remove empty modifiers // remove empty modifiers
if (_.isEmpty(modifier.$set)) { if (isEmpty(modifier.$set)) {
delete modifier.$set; delete modifier.$set;
} }
if (_.isEmpty(modifier.$unset)) { if (isEmpty(modifier.$unset)) {
delete modifier.$unset; delete modifier.$unset;
} }
if (!_.isEmpty(modifier)) { /*
DB Operation
*/
if (!isEmpty(modifier)) {
// update document // update document
await Connectors.update(collection, selector, modifier, { removeEmptyStrings: false }); await Connectors.update(collection, selector, modifier, { removeEmptyStrings: false });
// get fresh copy of document from db // get fresh copy of document from db
newDocument = await Connectors.get(collection, selector); document = await Connectors.get(collection, selector);
// TODO: add support for caching by other indexes to Dataloader // TODO: add support for caching by other indexes to Dataloader
// https://github.com/VulcanJS/Vulcan/issues/2000 // https://github.com/VulcanJS/Vulcan/issues/2000
@ -251,38 +294,40 @@ export const updateMutator = async ({ collection, documentId, selector, data, se
} }
} }
// run any post-operation sync callbacks /*
newDocument = await runCallbacks({ name: `${typeName.toLowerCase()}.update.after`, iterator: newDocument, properties: callbackProperties });
newDocument = await runCallbacks({ name: '*.update.after', iterator: newDocument, properties: callbackProperties });
// OpenCRUD backwards compatibility
newDocument = await runCallbacks(`${collectionName.toLowerCase()}.edit.after`, newDocument, document, currentUser);
After
*/
document = await runCallbacks({ name: `${typeName.toLowerCase()}.update.after`, iterator: document, properties });
document = await runCallbacks({ name: '*.update.after', iterator: document, properties });
// OpenCRUD backwards compatibility
document = await runCallbacks(`${collectionName.toLowerCase()}.edit.after`, document, oldDocument, currentUser);
/*
Async
*/
// run async callbacks // run async callbacks
await runCallbacksAsync({ name: `${typeName.toLowerCase()}.update.async`, properties: { ...callbackProperties,newDocument, document: newDocument, oldDocument: document }}); await runCallbacksAsync({ name: `${typeName.toLowerCase()}.update.async`, properties });
await runCallbacksAsync({ name: '*.update.async', properties: { ...callbackProperties, newDocument, document: newDocument, oldDocument: document }}); await runCallbacksAsync({ name: '*.update.async', properties });
// OpenCRUD backwards compatibility // OpenCRUD backwards compatibility
await runCallbacksAsync(`${collectionName.toLowerCase()}.edit.async`, newDocument, document, currentUser, collection); await runCallbacksAsync(`${collectionName.toLowerCase()}.edit.async`, document, oldDocument, currentUser, collection);
debug('\x1b[33m=> updated document with modifier: \x1b[0m'); endDebugMutator(collectionName, 'Update', { modifier });
debug('// modifier: ', modifier);
debugGroupEnd();
debug(`--------------- end \x1b[36m${collectionName} Update Mutator\x1b[0m ---------------`);
debug('');
return { data: newDocument }; return { data: document };
}; };
/*
Delete
*/
export const deleteMutator = async ({ collection, documentId, selector, currentUser, validate, context, document }) => { export const deleteMutator = async ({ collection, documentId, selector, currentUser, validate, context, document }) => {
const { collectionName, typeName } = collection.options; const { collectionName, typeName } = collection.options;
debug('');
debugGroup(`--------------- start \x1b[36m${collectionName} Delete Mutator\x1b[0m ---------------`);
debug('// collectionName: ', collectionName);
debug('// selector: ', selector);
const schema = collection.simpleSchema()._schema; const schema = collection.simpleSchema()._schema;
// OpenCRUD backwards compatibility // OpenCRUD backwards compatibility
selector = selector || { _id: documentId }; selector = selector || { _id: documentId };
@ -290,51 +335,68 @@ export const deleteMutator = async ({ collection, documentId, selector, currentU
throw new Error('Selector cannot be empty'); throw new Error('Selector cannot be empty');
} }
document = document || await Connectors.get(collection, selector); document = document || (await Connectors.get(collection, selector));
if (!document) { if (!document) {
throw new Error(`Could not find document to delete for selector: ${JSON.stringify(selector)}`); throw new Error(`Could not find document to delete for selector: ${JSON.stringify(selector)}`);
} }
const callbackProperties = { document, currentUser, collection, context }; /*
Properties
*/
const properties = { document, currentUser, collection, context };
/*
Validation
*/
if (validate) { if (validate) {
let validationErrors = []; let validationErrors = [];
validationErrors = await runCallbacks({ name: `${typeName.toLowerCase()}.delete.validate`, iterator: validationErrors, properties: callbackProperties }); validationErrors = await runCallbacks({ name: `${typeName.toLowerCase()}.delete.validate`, iterator: validationErrors, properties });
validationErrors = await runCallbacks({ name: '*.delete.validate', iterator: validationErrors, properties: callbackProperties }); validationErrors = await runCallbacks({ name: '*.delete.validate', iterator: validationErrors, properties });
// OpenCRUD backwards compatibility // OpenCRUD backwards compatibility
document = await runCallbacks(`${collectionName.toLowerCase()}.remove.validate`, document, currentUser); document = await runCallbacks(`${collectionName.toLowerCase()}.remove.validate`, document, currentUser);
if (validationErrors.length) { if (validationErrors.length) {
// eslint-disable-next-line no-console console.log(validationErrors); // eslint-disable-line no-console
console.log('// validationErrors'); throwError({ id: 'app.validation_error', data: { break: true, errors: validationErrors } });
// eslint-disable-next-line no-console
console.log(validationErrors);
throwError({id: 'app.validation_error', data: {break: true, errors: validationErrors}});
} }
} }
// run onRemove step /*
for(let fieldName of Object.keys(schema)) {
onDelete
*/
for (let fieldName of Object.keys(schema)) {
if (schema[fieldName].onDelete) { if (schema[fieldName].onDelete) {
// eslint-disable-next-line no-await-in-loop await schema[fieldName].onDelete(properties); // eslint-disable-line no-await-in-loop
await schema[fieldName].onDelete({ document, currentUser, context });
} else if (schema[fieldName].onRemove) { } else if (schema[fieldName].onRemove) {
// OpenCRUD backwards compatibility // OpenCRUD backwards compatibility
// eslint-disable-next-line no-await-in-loop await schema[fieldName].onRemove(document, currentUser); // eslint-disable-line no-await-in-loop
await schema[fieldName].onRemove(document, currentUser);
} }
} }
await runCallbacks({ name: `${typeName.toLowerCase()}.delete.before`, iterator: document, properties: callbackProperties }); /*
await runCallbacks({ name: '*.delete.before', iterator: document, properties: callbackProperties });
Before
*/
await runCallbacks({ name: `${typeName.toLowerCase()}.delete.before`, iterator: document, properties });
await runCallbacks({ name: '*.delete.before', iterator: document, properties });
// OpenCRUD backwards compatibility // OpenCRUD backwards compatibility
await runCallbacks(`${collectionName.toLowerCase()}.remove.before`, document, currentUser); await runCallbacks(`${collectionName.toLowerCase()}.remove.before`, document, currentUser);
await runCallbacks(`${collectionName.toLowerCase()}.remove.sync`, document, currentUser); await runCallbacks(`${collectionName.toLowerCase()}.remove.sync`, document, currentUser);
/*
DB Operation
*/
await Connectors.delete(collection, selector); await Connectors.delete(collection, selector);
// TODO: add support for caching by other indexes to Dataloader // TODO: add support for caching by other indexes to Dataloader
@ -343,14 +405,17 @@ export const deleteMutator = async ({ collection, documentId, selector, currentU
collection.loader.clear(selector.documentId); collection.loader.clear(selector.documentId);
} }
await runCallbacksAsync({ name: `${typeName.toLowerCase()}.delete.async`, properties: callbackProperties }); /*
await runCallbacksAsync({ name: '*.delete.async', properties: callbackProperties });
Async
*/
await runCallbacksAsync({ name: `${typeName.toLowerCase()}.delete.async`, properties });
await runCallbacksAsync({ name: '*.delete.async', properties });
// OpenCRUD backwards compatibility // OpenCRUD backwards compatibility
await runCallbacksAsync(`${collectionName.toLowerCase()}.remove.async`, document, currentUser, collection); await runCallbacksAsync(`${collectionName.toLowerCase()}.remove.async`, document, currentUser, collection);
debugGroupEnd(); endDebugMutator(collectionName, 'Delete');
debug(`--------------- end \x1b[36m${collectionName} Delete Mutator\x1b[0m ---------------`);
debug('');
return { data: document }; return { data: document };
}; };
@ -362,3 +427,20 @@ export const removeMutation = deleteMutator;
export const newMutator = createMutator; export const newMutator = createMutator;
export const editMutator = updateMutator; export const editMutator = updateMutator;
export const removeMutator = deleteMutator; export const removeMutator = deleteMutator;
const startDebugMutator = (name, action, properties) => {
debug('');
debugGroup(`--------------- start \x1b[36m${name} ${action} Mutator\x1b[0m ---------------`);
Object.keys(properties).forEach(p => {
debug(`// ${p}: `, properties[p]);
});
};
const endDebugMutator = (name, action, properties) => {
Object.keys(properties).forEach(p => {
debug(`// ${p}: `, properties[p]);
});
debugGroupEnd();
debug(`--------------- end \x1b[36m${name} ${action} Mutator\x1b[0m ---------------`);
debug('');
};

View file

@ -1,8 +1,11 @@
import React from 'react'; import React from 'react';
import { Checkbox } from 'formsy-react-components'; import Form from 'react-bootstrap/lib/Form';
import { registerComponent } from 'meteor/vulcan:core'; import { Components, registerComponent } from 'meteor/vulcan:core';
const CheckboxComponent = ({refFunction, inputProperties}) => const CheckboxComponent = ({ refFunction, path, inputProperties, itemProperties }) => (
<Checkbox {...inputProperties} ref={refFunction} />; <Components.FormItem {...inputProperties} {...itemProperties}>
<Form.Check {...inputProperties} id={path} ref={refFunction} checked={!!inputProperties.value}/>
</Components.FormItem>
);
registerComponent('FormComponentCheckbox', CheckboxComponent); registerComponent('FormComponentCheckbox', CheckboxComponent);

View file

@ -1,12 +1,12 @@
import React from 'react'; import React from 'react';
import { Checkbox } from 'formsy-react-components'; import Form from 'react-bootstrap/lib/Form';
import { registerComponent } from 'meteor/vulcan:core'; import { Components, registerComponent } from 'meteor/vulcan:core';
import without from 'lodash/without'; import without from 'lodash/without';
import uniq from 'lodash/uniq'; import uniq from 'lodash/uniq';
import intersection from 'lodash/intersection'; import intersection from 'lodash/intersection';
// note: treat checkbox group the same as a nested component, using `path` // note: treat checkbox group the same as a nested component, using `path`
const CheckboxGroupComponent = ({ refFunction, label, path, value, formType, updateCurrentValues, inputProperties }) => { const CheckboxGroupComponent = ({ refFunction, label, path, value, formType, updateCurrentValues, inputProperties, itemProperties }) => {
const { options } = inputProperties; const { options } = inputProperties;
@ -22,25 +22,28 @@ const CheckboxGroupComponent = ({ refFunction, label, path, value, formType, upd
} }
return ( return (
<div className="form-group row"> <Components.FormItem {...inputProperties} {...itemProperties}>
<label className="control-label col-sm-3">{label}</label> <div>
<div className="col-sm-9">
{options.map((option, i) => ( {options.map((option, i) => (
<Checkbox <Form.Check
layout="elementOnly" layout="elementOnly"
key={i} key={i}
{...inputProperties} {...inputProperties}
label={option.label} label={option.label}
value={value.includes(option.value)} value={value.includes(option.value)}
checked={!!value.includes(option.value)}
id={`${path}.${i}`}
path={`${path}.${i}`}
ref={refFunction} ref={refFunction}
onChange={(name, isChecked) => { onChange={event => {
const isChecked = event.target.checked;
const newValue = isChecked ? [...value, option.value] : without(value, option.value); const newValue = isChecked ? [...value, option.value] : without(value, option.value);
updateCurrentValues({ [path]: newValue }); updateCurrentValues({ [path]: newValue });
}} }}
/> />
))} ))}
</div> </div>
</div> </Components.FormItem>
); );
}; };

View file

@ -1,43 +1,34 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import DateTimePicker from 'react-datetime'; import DateTimePicker from 'react-datetime';
import { registerComponent } from 'meteor/vulcan:core'; import { Components, registerComponent } from 'meteor/vulcan:core';
class DateComponent extends PureComponent { class DateComponent extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.updateDate = this.updateDate.bind(this); this.updateDate = this.updateDate.bind(this);
} }
// when the datetime picker has mounted, SmartForm will catch the date value (no formsy mixin in this component)
// componentDidMount() {
// if (this.props.value) {
// this.updateDate(this.props.value);
// }
// }
updateDate(date) { updateDate(date) {
this.context.updateCurrentValues({[this.props.path]: date}); this.context.updateCurrentValues({ [this.props.path]: date });
} }
render() { render() {
const date = this.props.value
const date = this.props.value ? (typeof this.props.value === 'string' ? new Date(this.props.value) : this.props.value) : null; ? typeof this.props.value === 'string'
? new Date(this.props.value)
: this.props.value
: null;
return ( return (
<div className="form-group row"> <Components.FormItem {...this.props.inputProperties} {...this.props.itemProperties}>
<label className="control-label col-sm-3">{this.props.label}</label> <DateTimePicker
<div className="col-sm-9"> value={date}
<DateTimePicker timeFormat={false}
value={date} // newDate argument is a Moment object given by react-datetime
timeFormat={false} onChange={newDate => this.updateDate(newDate)}
// newDate argument is a Moment object given by react-datetime inputProps={{ name: this.props.name }}
onChange={newDate => this.updateDate(newDate)} />
inputProps={{name: this.props.name}} </Components.FormItem>
/>
</div>
</div>
); );
} }
} }
@ -57,4 +48,4 @@ DateComponent.contextTypes = {
export default DateComponent; export default DateComponent;
registerComponent('FormComponentDate', DateComponent); registerComponent('FormComponentDate', DateComponent);

View file

@ -2,17 +2,20 @@ import React, { PureComponent } from 'react';
import { Components, registerComponent } from 'meteor/vulcan:core'; import { Components, registerComponent } from 'meteor/vulcan:core';
import moment from 'moment'; import moment from 'moment';
const isEmptyValue = value => (typeof value === 'undefined' || value === null || value === '' || Array.isArray(value) && value.length === 0); const isEmptyValue = value =>
typeof value === 'undefined' ||
value === null ||
value === '' ||
(Array.isArray(value) && value.length === 0);
class DateComponent2 extends PureComponent { class DateComponent2 extends PureComponent {
state = { state = {
year: null, year: null,
month: null, month: null,
day: null, day: null,
} };
updateDate = (date) => { updateDate = date => {
const { value, path } = this.props; const { value, path } = this.props;
let newDate; let newDate;
this.setState(date, () => { this.setState(date, () => {
@ -20,7 +23,10 @@ class DateComponent2 extends PureComponent {
if (isEmptyValue(value)) { if (isEmptyValue(value)) {
if (year && month && day) { if (year && month && day) {
// wait until we have all three values to update the date // wait until we have all three values to update the date
newDate = moment().year(year).month(month).date(day); newDate = moment()
.year(year)
.month(month)
.date(day);
this.props.updateCurrentValues({ [path]: newDate.toDate() }); this.props.updateCurrentValues({ [path]: newDate.toDate() });
} }
} else { } else {
@ -32,10 +38,9 @@ class DateComponent2 extends PureComponent {
this.props.updateCurrentValues({ [path]: newDate.toDate() }); this.props.updateCurrentValues({ [path]: newDate.toDate() });
} }
}); });
} };
render() { render() {
const { path, value } = this.props; const { path, value } = this.props;
const months = moment.months(); const months = moment.months();
const mDate = !isEmptyValue(value) && moment(value); const mDate = !isEmptyValue(value) && moment(value);
@ -45,10 +50,10 @@ class DateComponent2 extends PureComponent {
name: `${path}.month`, name: `${path}.month`,
layout: 'vertical', layout: 'vertical',
options: months.map((m, i) => ({ label: m, value: m })), options: months.map((m, i) => ({ label: m, value: m })),
value: mDate && mDate.format('MMMM') || '', value: (mDate && mDate.format('MMMM')) || '',
onChange: (name, value) => { onChange: (name, value) => {
this.updateDate({ month: value }); this.updateDate({ month: value });
} },
}; };
const dayProperties = { const dayProperties = {
@ -56,10 +61,10 @@ class DateComponent2 extends PureComponent {
name: `${path}.day`, name: `${path}.day`,
layout: 'vertical', layout: 'vertical',
maxLength: 2, maxLength: 2,
value: mDate && mDate.format('DD') || '', value: (mDate && mDate.format('DD')) || '',
onBlur: (e) => { onBlur: e => {
this.updateDate({ day: e.target.value }); this.updateDate({ day: e.target.value });
} },
}; };
const yearProperties = { const yearProperties = {
@ -67,25 +72,33 @@ class DateComponent2 extends PureComponent {
name: `${path}.year`, name: `${path}.year`,
layout: 'vertical', layout: 'vertical',
maxLength: 4, maxLength: 4,
value: mDate && mDate.format('YYYY') || '', value: (mDate && mDate.format('YYYY')) || '',
onBlur: (e) => { onBlur: e => {
this.updateDate({ year: e.target.value }); this.updateDate({ year: e.target.value });
} },
}; };
return ( return (
<div className="form-group row"> <Components.FormItem {...this.props.inputProperties} {...this.props.itemProperties}>
<label className="control-label col-sm-3">{this.props.label}</label> <div>
<div className="col-sm-9" style={{ display: 'flex', alignItems: 'center' }}> <div>
<div><Components.FormComponentSelect inputProperties={monthProperties} datatype={[{ type: String }]} /></div> <Components.FormComponentSelect
<div style={{ marginLeft: 10, width: 60 }}><Components.FormComponentText inputProperties={dayProperties} /></div> inputProperties={monthProperties}
<div style={{ marginLeft: 10, width: 80 }}><Components.FormComponentText inputProperties={yearProperties} /></div> datatype={[{ type: String }]}
/>
</div>
<div style={{ marginLeft: 10, width: 60 }}>
<Components.FormComponentText inputProperties={dayProperties} />
</div>
<div style={{ marginLeft: 10, width: 80 }}>
<Components.FormComponentText inputProperties={yearProperties} />
</div>
</div> </div>
</div> </Components.FormItem>
); );
} }
} }
export default DateComponent2; export default DateComponent2;
registerComponent('FormComponentDate2', DateComponent2); registerComponent('FormComponentDate2', DateComponent2);

View file

@ -1,43 +1,35 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import DateTimePicker from 'react-datetime'; import DateTimePicker from 'react-datetime';
import { registerComponent } from 'meteor/vulcan:core'; import { Components, registerComponent } from 'meteor/vulcan:core';
class DateTime extends PureComponent { class DateTime extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.updateDate = this.updateDate.bind(this); this.updateDate = this.updateDate.bind(this);
} }
// when the datetime picker has mounted, SmartForm will catch the date value (no formsy mixin in this component)
// componentDidMount() {
// if (this.props.value) {
// this.updateDate(this.props.value);
// }
// }
updateDate(date) { updateDate(date) {
this.context.updateCurrentValues({[this.props.path]: date}); this.context.updateCurrentValues({ [this.props.path]: date });
} }
render() { render() {
const date = this.props.value
? typeof this.props.value === 'string'
? new Date(this.props.value)
: this.props.value
: null;
const date = this.props.value ? (typeof this.props.value === 'string' ? new Date(this.props.value) : this.props.value) : null;
return ( return (
<div className="form-group row"> <Components.FormItem {...this.props.inputProperties} {...this.props.itemProperties}>
<label className="control-label col-sm-3">{this.props.label}</label> <DateTimePicker
<div className="col-sm-9"> value={date}
<DateTimePicker // newDate argument is a Moment object given by react-datetime
value={date} onChange={newDate => this.updateDate(newDate._d)}
// newDate argument is a Moment object given by react-datetime format={'x'}
onChange={newDate => this.updateDate(newDate._d)} inputProps={{ name: this.props.name }}
format={'x'} />
inputProps={{name: this.props.name}} </Components.FormItem>
/>
</div>
</div>
); );
} }
} }
@ -57,4 +49,4 @@ DateTime.contextTypes = {
export default DateTime; export default DateTime;
registerComponent('FormComponentDateTime', DateTime); registerComponent('FormComponentDateTime', DateTime);

View file

@ -1,9 +1,12 @@
import React from 'react'; import React from 'react';
import { Input } from 'formsy-react-components'; import Form from 'react-bootstrap/lib/Form';
import { registerComponent } from 'meteor/vulcan:core'; import { Components, registerComponent } from 'meteor/vulcan:core';
const Default = ({refFunction, inputProperties}) => const Default = ({ refFunction, inputProperties, itemProperties }) => (
<Input {...inputProperties} ref={refFunction} type="text" />; <Components.FormItem {...inputProperties} {...itemProperties}>
<Form.Control {...inputProperties} ref={refFunction} type="text" />
</Components.FormItem>
);
registerComponent('FormComponentDefault', Default); registerComponent('FormComponentDefault', Default);
registerComponent('FormComponentText', Default); registerComponent('FormComponentText', Default);

View file

@ -1,8 +1,11 @@
import React from 'react'; import React from 'react';
import { Input } from 'formsy-react-components'; import Form from 'react-bootstrap/lib/Form';
import { registerComponent } from 'meteor/vulcan:core'; import { Components, registerComponent } from 'meteor/vulcan:core';
const EmailComponent = ({refFunction, inputProperties}) => const EmailComponent = ({ refFunction, inputProperties, itemProperties }) => (
<Input {...inputProperties} ref={refFunction} type="email" />; <Components.FormItem {...inputProperties} {...itemProperties}>
<Form.Control {...inputProperties} ref={refFunction} type="email" />
</Components.FormItem>
);
registerComponent('FormComponentEmail', EmailComponent); registerComponent('FormComponentEmail', EmailComponent);

View file

@ -21,15 +21,20 @@ class FormComponentInner extends PureComponent {
}; };
getProperties = () => { getProperties = () => {
const { name, options, label, onChange, value, disabled } = this.props; const { name, path, options, label, onChange, value, disabled, inputType } = this.props;
// these properties are whitelisted so that they can be safely passed to the actual form input // these properties are whitelisted so that they can be safely passed to the actual form input
// and avoid https://facebook.github.io/react/warnings/unknown-prop.html warnings // and avoid https://facebook.github.io/react/warnings/unknown-prop.html warnings
const inputProperties = { const inputProperties = {
name, name,
path,
options, options,
label, label,
onChange, onChange: event => {
// FormComponent's handleChange expects value as argument; look in target.checked or target.value
const inputValue = inputType === 'checkbox' ? event.target.checked : event.target.value;
onChange(inputValue);
},
value, value,
disabled, disabled,
...this.props.inputProperties, ...this.props.inputProperties,

View file

@ -0,0 +1,5 @@
// import React from 'react';
import Form from 'react-bootstrap/lib/Form';
import { registerComponent } from 'meteor/vulcan:core';
registerComponent('FormElement', Form);

View file

@ -0,0 +1,38 @@
/*
Layout for a single form item
*/
import React from 'react';
import Form from 'react-bootstrap/lib/Form';
import Row from 'react-bootstrap/lib/Row';
import Col from 'react-bootstrap/lib/Col';
import { registerComponent } from 'meteor/vulcan:core';
const FormItem = ({ path, label, children, beforeInput, afterInput }) => {
if (label) {
return (
<Form.Group as={Row} controlId={path}>
<Form.Label column sm={3}>
{label}
</Form.Label>
<Col sm={9}>
{beforeInput}
{children}
{afterInput}
</Col>
</Form.Group>
);
} else {
return (
<Form.Group controlId={path}>
{beforeInput}
{children}
{afterInput}
</Form.Group>
);
}
};
registerComponent('FormItem', FormItem);

View file

@ -1,8 +1,11 @@
import React from 'react'; import React from 'react';
import { Input } from 'formsy-react-components'; import Form from 'react-bootstrap/lib/Form';
import { registerComponent } from 'meteor/vulcan:core'; import { Components, registerComponent } from 'meteor/vulcan:core';
const NumberComponent = ({refFunction, inputProperties}) => const NumberComponent = ({ refFunction, inputProperties, itemProperties }) => (
<Input {...inputProperties} ref={refFunction} type="number" />; <Components.FormItem {...inputProperties} {...itemProperties}>
<Form.Control {...inputProperties} ref={refFunction} type="number" />
</Components.FormItem>
);
registerComponent('FormComponentNumber', NumberComponent); registerComponent('FormComponentNumber', NumberComponent);

View file

@ -1,7 +1,11 @@
import React from 'react'; import React from 'react';
import { RadioGroup } from 'formsy-react-components'; import Form from 'react-bootstrap/lib/Form';
import { registerComponent } from 'meteor/vulcan:core'; import { Components, registerComponent } from 'meteor/vulcan:core';
const RadioGroupComponent = ({refFunction, inputProperties, ...properties}) => <RadioGroup {...inputProperties} ref={refFunction}/>; const RadioGroupComponent = ({ refFunction, inputProperties, itemProperties }) => (
<Components.FormItem {...inputProperties} {...itemProperties}>
<Form.Check {...inputProperties} ref={refFunction} />
</Components.FormItem>
);
registerComponent('FormComponentRadioGroup', RadioGroupComponent); registerComponent('FormComponentRadioGroup', RadioGroupComponent);

View file

@ -1,20 +1,31 @@
import React from 'react'; import React from 'react';
import { intlShape } from 'meteor/vulcan:i18n'; import { intlShape } from 'meteor/vulcan:i18n';
import { Select } from 'formsy-react-components'; import Form from 'react-bootstrap/lib/Form';
import { registerComponent } from 'meteor/vulcan:core'; import { Components, registerComponent } from 'meteor/vulcan:core';
// copied from vulcan:forms/utils.js to avoid extra dependency // copied from vulcan:forms/utils.js to avoid extra dependency
const getFieldType = datatype => datatype && datatype[0].type; const getFieldType = datatype => datatype && datatype[0].type;
const SelectComponent = ({refFunction, inputProperties, datatype, ...properties}, { intl }) => { const SelectComponent = ({ refFunction, inputProperties, itemProperties, datatype }, { intl }) => {
const noneOption = { const noneOption = {
label: intl.formatMessage({ id: 'forms.select_option' }), label: intl.formatMessage({ id: 'forms.select_option' }),
value: getFieldType(datatype) === String || getFieldType(datatype) === Number ? '' : null, // depending on field type, empty value can be '' or null value: getFieldType(datatype) === String || getFieldType(datatype) === Number ? '' : null, // depending on field type, empty value can be '' or null
disabled: true, disabled: true,
}; };
let otherOptions = Array.isArray(inputProperties.options) && inputProperties.options.length ? inputProperties.options : []; let otherOptions =
Array.isArray(inputProperties.options) && inputProperties.options.length
? inputProperties.options
: [];
const options = [noneOption, ...otherOptions]; const options = [noneOption, ...otherOptions];
return <Select {...inputProperties} options={options} ref={refFunction}/>; return (
<Components.FormItem {...inputProperties} {...itemProperties}>
<Form.Control as="select" {...inputProperties} ref={refFunction}>
{options.map((option, i) => (
<option key={i} {...option}>{option.value}</option>
))}
</Form.Control>
</Components.FormItem>
);
}; };
SelectComponent.contextTypes = { SelectComponent.contextTypes = {

View file

@ -1,12 +1,16 @@
import React from 'react'; import React from 'react';
import { intlShape } from 'meteor/vulcan:i18n'; import { intlShape } from 'meteor/vulcan:i18n';
import { Select } from 'formsy-react-components'; import Form from 'react-bootstrap/lib/Form';
import { registerComponent } from 'meteor/vulcan:core'; import { Components, registerComponent } from 'meteor/vulcan:core';
const SelectMultipleComponent = ({refFunction, inputProperties, ...properties}, { intl }) => { const SelectMultipleComponent = ({ refFunction, inputProperties, itemProperties }, { intl }) => {
inputProperties.multiple = true; inputProperties.multiple = true;
return <Select {...inputProperties} ref={refFunction}/>; return (
<Components.FormItem {...inputProperties} {...itemProperties}>
<Form.Control as="select" {...inputProperties} ref={refFunction} />
</Components.FormItem>
);
}; };
SelectMultipleComponent.contextTypes = { SelectMultipleComponent.contextTypes = {

View file

@ -1,15 +1,20 @@
import React from 'react'; import React from 'react';
import { registerComponent } from 'meteor/vulcan:core'; import { Components, registerComponent } from 'meteor/vulcan:core';
const parseUrl = value => { const parseUrl = value => {
return value && value.toString().slice(0,4) === 'http' ? <a href={value} target="_blank">{value}</a> : value; return value && value.toString().slice(0, 4) === 'http' ? (
<a href={value} target="_blank">
{value}
</a>
) : (
value
);
}; };
const StaticComponent = ({ value, label }) => ( const StaticComponent = ({ inputProperties, itemProperties }) => (
<div className="form-group row"> <Components.FormItem {...inputProperties} {...itemProperties}>
<label className="control-label col-sm-3">{label}</label> <div>{parseUrl(inputProperties.value)}</div>
<div className="col-sm-9">{parseUrl(value)}</div> </Components.FormItem>
</div>
); );
registerComponent('FormComponentStaticText', StaticComponent); registerComponent('FormComponentStaticText', StaticComponent);

View file

@ -1,7 +1,11 @@
import React from 'react'; import React from 'react';
import { Textarea } from 'formsy-react-components'; import Form from 'react-bootstrap/lib/Form';
import { registerComponent } from 'meteor/vulcan:core'; import { Components, registerComponent } from 'meteor/vulcan:core';
const TextareaComponent = ({refFunction, inputProperties, ...properties}) => <Textarea ref={refFunction} {...inputProperties}/>; const TextareaComponent = ({ refFunction, inputProperties, itemProperties }) => (
<Components.FormItem {...inputProperties} {...itemProperties}>
<Form.Control as="textarea" ref={refFunction} {...inputProperties} />
</Components.FormItem>
);
registerComponent('FormComponentTextarea', TextareaComponent); registerComponent('FormComponentTextarea', TextareaComponent);

View file

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

View file

@ -1,7 +1,11 @@
import React from 'react'; import React from 'react';
import { Input } from 'formsy-react-components'; import Form from 'react-bootstrap/lib/Form';
import { registerComponent } from 'meteor/vulcan:core'; import { Components, registerComponent } from 'meteor/vulcan:core';
const UrlComponent = ({refFunction, inputProperties, ...properties}) => <Input ref={refFunction} {...inputProperties} type="url" />; const UrlComponent = ({ refFunction, inputProperties, itemProperties }) => (
<Components.FormItem {...inputProperties} {...itemProperties}>
<Form.Control ref={refFunction} {...inputProperties} {...itemProperties} type="url" />
</Components.FormItem>
);
registerComponent('FormComponentUrl', UrlComponent); registerComponent('FormComponentUrl', UrlComponent);

View file

@ -1,3 +1,4 @@
import '../components/forms/FormElement.jsx';
import '../components/forms/Checkbox.jsx'; import '../components/forms/Checkbox.jsx';
import '../components/forms/Checkboxgroup.jsx'; import '../components/forms/Checkboxgroup.jsx';
import '../components/forms/Date.jsx'; import '../components/forms/Date.jsx';
@ -15,6 +16,7 @@ import '../components/forms/Url.jsx';
import '../components/forms/StaticText.jsx'; import '../components/forms/StaticText.jsx';
import '../components/forms/FormComponentInner.jsx'; import '../components/forms/FormComponentInner.jsx';
import '../components/forms/FormControl.jsx'; // note: only used by old accounts package, remove soon? import '../components/forms/FormControl.jsx'; // note: only used by old accounts package, remove soon?
import '../components/forms/FormItem.jsx';
import '../components/ui/Button.jsx'; import '../components/ui/Button.jsx';
import '../components/ui/Alert.jsx'; import '../components/ui/Alert.jsx';