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": [1, "always", { "objectsInObjects": false, "arraysInObjects": false }],
"babel/object-shorthand": 0,
"babel/arrow-parens": 0,
"no-await-in-loop": 1,
@ -78,4 +79,4 @@
"param": true,
"returns": true
}
}
}

View file

@ -3,11 +3,11 @@
const {esNextPaths} = require('./.vulcan/shared/pathsByLanguageVersion');
module.exports = {
bracketSpacing: false,
bracketSpacing: true,
singleQuote: true,
jsxBracketSameLine: true,
trailingComma: 'es5',
printWidth: 80,
printWidth: 100,
parser: 'babylon',
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": "npm run test-unit",
"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": {
"hooks": {
@ -105,10 +106,13 @@
},
"private": true,
"devDependencies": {
"@userfrosting/merge-package-dependencies": "^1.2.0",
"autoprefixer": "^6.3.6",
"babel-eslint": "^7.0.0",
"babylon": "^6.18.0",
"colors": "^1.3.2",
"chromedriver": "^2.40.0",
"diff": "^3.5.0",
"enzyme": "^3.3.0",
"enzyme-adapter-react-16.3": "^1.4.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;
message = _.isObject(message) ? message.message : message; // If message is object, then try to get message from it
return message ? (
<div style={style} className={[ className, type ].join(' ')}>{ message }</div>
<div style={style} className={[className, type].join(' ')}>{ message }</div>
) : 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 './routes.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';
addRoute({ name: 'admin', path: '/admin', component: () => getDynamicComponent(import('../components/AdminHome.jsx'))});
addRoute({ name: 'admin2', path: '/admin/users', component: () => getDynamicComponent(import('../components/AdminHome.jsx'))});
addRoute({
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 { withRouter} from 'react-router';
const DummyErrorCatcher = ({ children }) => children;
// see https://stackoverflow.com/questions/42862028/react-router-v4-with-multiple-layouts
const RouteWithLayout = ({ layoutName, component, currentRoute, ...rest }) => {
// 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 (
<Route
@ -38,8 +36,8 @@ const RouteWithLayout = ({ layoutName, component, currentRoute, ...rest }) => {
{...rest}
render={props => {
const layoutProps = {...props, currentRoute}
const childComponentProps = {...props, currentRoute}
const layoutProps = { ...props, currentRoute };
const childComponentProps = { ...props, currentRoute };
const layout = layoutName ? Components[layoutName] : Components.Layout;
return React.createElement(layout, layoutProps, <ErrorCatcher>{React.createElement(component, childComponentProps)}</ErrorCatcher>);
}}

View file

@ -187,4 +187,4 @@ Card.contextTypes = {
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
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 }} />
</Components.Alert>
);

View file

@ -1,31 +1,63 @@
import React, { PureComponent } from 'react';
import { withCurrentUser } from 'meteor/vulcan:core';
import { withRouter } from 'react-router';
import React, {PureComponent} from 'react';
import {Components} from 'meteor/vulcan:lib';
import withCurrentUser from './withCurrentUser';
import {withRouter} from 'react-router';
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
return WrappedComponent => {
class AccessComponent extends PureComponent {
// if there are any groups defined check if user belongs, else just check if user exists
canAccess = currentUser => {
return groups ? Users.isMemberOf(currentUser, groups) : currentUser;
}
};
// redirect on constructor if user cannot access
constructor(props) {
super(props);
if(!this.canAccess(props.currentUser) && typeof redirect === 'string') {
if (
!this.canAccess(props.currentUser) &&
typeof redirect === 'string'
) {
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() {
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) {
registerCallback({
name: `${typeName}.create.validate`,
iterator: {document: 'The document being inserted'},
iterator: { validationErrors: 'An array that can be used to accumulate validation errors' },
properties: [
{document: 'The document being inserted'},
{currentUser: 'The current user'},
{
validationErrors:
'An object that can be used to accumulate validation errors',
},
{ document: 'The document being inserted' },
{ currentUser: 'The current user' },
{ collection: 'The collection the document belongs to' },
{ context: 'The context of the mutation'},
],
runs: 'sync',
returns: 'document',
@ -467,14 +465,13 @@ const registerCollectionCallbacks = (typeName, options) => {
if (options.update) {
registerCallback({
name: `${typeName}.update.validate`,
iterator: {data: 'The client data'},
iterator: { validationErrors: 'An object that can be used to accumulate validation errors' },
properties: [
{document: 'The document being edited'},
{currentUser: 'The current user'},
{
validationErrors:
'An object that can be used to accumulate validation errors',
},
{ document: 'The document being edited' },
{ data: 'The client data' },
{ currentUser: 'The current user' },
{ collection: 'The collection the document belongs to' },
{ context: 'The context of the mutation'},
],
runs: 'sync',
returns: 'modifier',
@ -522,13 +519,12 @@ const registerCollectionCallbacks = (typeName, options) => {
if (options.delete) {
registerCallback({
name: `${typeName}.delete.validate`,
iterator: {document: 'The document being removed'},
iterator: { validationErrors: 'An object that can be used to accumulate validation errors' },
properties: [
{currentUser: 'The current user'},
{
validationErrors:
'An object that can be used to accumulate validation errors',
},
{ currentUser: 'The current user' },
{ document: 'The document being removed' },
{ collection: 'The collection the document belongs to'},
{ context: 'The context of this mutation'}
],
runs: 'sync',
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 RouterHook } from './components/RouterHook.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 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 withMutation } from './containers/withMutation.js';
export { default as withUpsert } from './containers/withUpsert.js';
export { default as withSiteData } from './containers/withSiteData.js';
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 { Link } from 'react-router-dom';
const RoutePath = ({ document }) =>
<Link to={document.path}>{document.path}</Link>;
const RoutePath = ({document}) => (
<Link to={document.path}>{document.path}</Link>
);
const RoutesDashboard = props =>
<div className="routes">
<Components.Datatable
showSearch={false}
showNew={false}
showEdit={false}
data={Object.values(Routes)}
columns={[
'name',
{
name: 'path',
component: RoutePath
},
'componentName',
]}
/>
</div>;
const RoutesDashboard = props => {
return (
<div className="routes">
<Components.Datatable
showSearch={false}
showNew={false}
showEdit={false}
data={Object.values(Routes)}
columns={[
'name',
{
name: 'path',
component: RoutePath,
},
'componentName',
'layoutName'
]}
/>
</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/Groups.jsx';

View file

@ -1,14 +1,54 @@
import { addRoute, getDynamicComponent } from 'meteor/vulcan:lib';
import {addRoute, getDynamicComponent} from 'meteor/vulcan:lib';
addRoute([
// {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: 'debugSettings', path: '/debug/settings', componentName: 'Settings', layoutName: 'AdminLayout' },
{ name: 'debugCallbacks', path: '/debug/callbacks', componentName: 'Callbacks', layoutName: 'AdminLayout' },
{
name: 'debug',
path: '/debug',
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: 'debugEmails', path: '/debug/emails', componentName: 'Emails', layoutName: 'AdminLayout' },
{ name: 'debugRoutes', path: '/debug/routes', componentName: 'Routes', layoutName: 'AdminLayout' },
{ name: 'debugComponents', path: '/debug/components', componentName: 'Components', layoutName: 'AdminLayout' },
{ name: 'debugI18n', path: '/debug/i18n', componentName: 'I18n', layoutName: 'AdminLayout' },
{
name: 'debugEmails',
path: '/debug/emails',
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 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 { 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: fix this error: Error: getaddrinfo ENOTFOUND
if (typeof to === 'object') {
// 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');
@ -102,14 +103,15 @@ VulcanEmail.send = (to, subject, html, text, throwErrors, cc, bcc, replyTo, head
const email = {
from: from,
to: to,
cc: cc,
bcc: bcc,
replyTo: replyTo,
subject: subject,
headers: headers,
text: text,
html: html,
to,
cc,
bcc,
replyTo,
subject,
headers,
text,
html,
attachments,
};
const shouldSendEmail = process.env.NODE_ENV === 'production' || getSetting('enableDevelopmentEmails', false);
@ -153,9 +155,9 @@ VulcanEmail.build = async ({ emailName, variables, locale }) => {
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 });
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));

View file

@ -3,9 +3,7 @@ import { withMutation } from 'meteor/vulcan:core';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, intlShape } from 'meteor/vulcan:i18n';
import FRC from 'formsy-react-components';
const Input = FRC.Input;
import Form from 'react-bootstrap/lib/Form';
class EmbedURL extends Component {
constructor(props) {
@ -161,7 +159,7 @@ class EmbedURL extends Component {
<label className="control-label col-sm-3">{this.props.label}</label>
<div className="col-sm-9 embedly-form-control">
<div className="embedly-url-field">
<Input
<Form.Control
{...inputProperties}
onBlur={this.handleBlur}
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 { 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() {
const { error } = this.state;
return error ? (
<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>
) : (
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 PropTypes from 'prop-types';
import { intlShape } from 'meteor/vulcan:i18n';
import Formsy from 'formsy-react';
import cloneDeep from 'lodash/cloneDeep';
import get from 'lodash/get';
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.
*/
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 {
schema = this.state.schema,
excludeHiddenFields = true,
replaceIntlFields = false,
addExtraFields = true
} = args;
} = args0;
const { fields, addFields } = this.props;
@ -619,7 +620,7 @@ class SmartForm extends Component {
...newValues
} // Submit form after setState update completed
}),
() => this.submitForm(this.form.getModel())
() => this.submitForm()
);
};
@ -856,7 +857,7 @@ class SmartForm extends Component {
*/
formKeyDown = event => {
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
*/
submitForm = data => {
// note: we can discard the data collected by Formsy because all the data we need is already available via getDocument()
submitForm = event => {
event && event.preventDefault();
// if form is disabled (there is already a submit handler running) don't do anything
if (this.state.disabled) {
return;
@ -931,9 +933,9 @@ class SmartForm extends Component {
// clear errors and disable form while it's submitting
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
data = this.getData({ replaceIntlFields: true, addExtraFields: false });
let data = this.getData({ replaceIntlFields: true, addExtraFields: false });
// if there's a submit callback, run it
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() {
const fieldGroups = this.getFieldGroups();
const collectionName = this.props.collectionName;
const FormComponents = mergeWithComponents(this.props.formComponents);
return (
<div className={'document-' + this.getFormType()}>
<Formsy.Form
id={this.props.id}
onSubmit={this.submitForm}
onKeyDown={this.formKeyDown}
ref={e => {
this.form = e;
}}
>
<FormComponents.FormErrors errors={this.state.errors} />
<FormComponents.FormElement {...this.getFormProps()}>
<FormComponents.FormErrors {...this.getFormErrorsProps()} />
{fieldGroups.map(group => (
<FormComponents.FormGroup
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.getFieldGroups().map(group => (
<FormComponents.FormGroup {...this.getFormGroupProps(group)} />
))}
{this.props.repeatErrors && this.renderErrors()}
{this.props.repeatErrors && this.renderErrors()}
<FormComponents.FormSubmit
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={collectionName}
currentValues={this.state.currentValues}
deletedValues={this.state.deletedValues}
errors={this.state.errors}
/>
</Formsy.Form>
</div>
<FormComponents.FormSubmit {...this.getFormSubmitProps()} />
</FormComponents.FormElement>
);
}
}

View file

@ -28,14 +28,14 @@ class FormComponent extends Component {
}
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
const includesPathOrChildren = deletedValues =>
deletedValues.some(deletedPath => deletedPath.includes(path));
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 deleteChanged =
includesPathOrChildren(deletedValues) !==
@ -93,7 +93,8 @@ class FormComponent extends Component {
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 === '') {
value = null;

View file

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

View file

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

View file

@ -51,6 +51,8 @@ import {
import withCollectionProps from './withCollectionProps';
import { callbackProps } from './propTypes';
const intlSuffix = '_intl';
class FormWrapper extends PureComponent {
constructor(props) {
super(props);
@ -94,8 +96,11 @@ class FormWrapper extends PureComponent {
// if "fields" prop is specified, restrict list of fields to it
if (typeof fields !== 'undefined' && fields.length > 0) {
queryFields = _.intersection(queryFields, fields);
mutationFields = _.intersection(mutationFields, fields);
// add "_intl" suffix to all fields in case some of them are intl 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
@ -105,7 +110,7 @@ class FormWrapper extends PureComponent {
}
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.

View file

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

View file

@ -13,7 +13,7 @@ import { delayedComponent } from './components';
* `Components.DynamicLoading` in the meantime.
*
* @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
* 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;
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 headers:');
// console.log(headers);

View file

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

View file

@ -1,25 +1,29 @@
/*
Mutations have four steps:
Mutations have five steps:
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:
- Check that the current user has permission to insert/edit each field.
- Add userId to document (insert only).
- 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
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.
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');
/*
Create
*/
export const createMutator = async ({ collection, document, data, currentUser, validate, context }) => {
const { collectionName, typeName } = collection.options;
debug('');
debugGroup(`--------------- start \x1b[36m${collectionName} Create Mutator\x1b[0m ---------------`);
debug(`validate: ${validate}`);
debug(document || data);
// OpenCRUD backwards compatibility: accept either data or 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 callbackProperties = { currentUser, collection, context };
startDebugMutator(collectionName, 'Update', { validate, document });
/*
Properties
*/
const properties = { data, currentUser, collection, context, document: document };
/*
Validation
*/
if (validate) {
let validationErrors = [];
validationErrors = validationErrors.concat(validateDocument(newDocument, collection, context));
validationErrors = validationErrors.concat(validateDocument(document, collection, context));
// run validation callbacks
validationErrors = await runCallbacks({ name: `${typeName.toLowerCase()}.create.validate`, iterator: validationErrors, properties: { ...callbackProperties, document: newDocument } });
validationErrors = await runCallbacks({ name: '*.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 });
// 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) {
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) {
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.
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
*/
for(let fieldName of Object.keys(schema)) {
for (let fieldName of Object.keys(schema)) {
let autoValue;
if (schema[fieldName].onCreate) {
// OpenCRUD backwards compatibility: keep both newDocument and data for now, but phase our newDocument eventually
// eslint-disable-next-line no-await-in-loop
autoValue = await schema[fieldName].onCreate({ newDocument: clone(newDocument), data: clone(newDocument), currentUser, context });
// OpenCRUD backwards compatibility: keep both newDocument and data for now, but phase out newDocument eventually
autoValue = await schema[fieldName].onCreate(properties); // eslint-disable-line no-await-in-loop
} else if (schema[fieldName].onInsert) {
// OpenCRUD backwards compatibility
// eslint-disable-next-line no-await-in-loop
autoValue = await schema[fieldName].onInsert(clone(newDocument), currentUser);
autoValue = await schema[fieldName].onInsert(clone(document), currentUser); // eslint-disable-line no-await-in-loop
}
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'];
// }
// 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
newDocument = await runCallbacks(`${collectionName.toLowerCase()}.new.before`, newDocument, currentUser);
newDocument = await runCallbacks(`${collectionName.toLowerCase()}.new.sync`, newDocument, currentUser);
document = await runCallbacks(`${collectionName.toLowerCase()}.new.before`, document, 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
newDocument = await runCallbacks({ name: `${typeName.toLowerCase()}.create.after`, iterator: newDocument, properties: callbackProperties });
newDocument = await runCallbacks({ name: '*.create.after', iterator: newDocument, properties: callbackProperties });
document = await runCallbacks({ name: `${typeName.toLowerCase()}.create.after`, iterator: document, properties });
document = await runCallbacks({ name: '*.create.after', iterator: document, properties });
// 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
await runCallbacksAsync({ name: `${typeName.toLowerCase()}.create.async`, properties: { insertedDocument, document: insertedDocument, ...callbackProperties }});
await runCallbacksAsync({ name: '*.create.async', properties: { insertedDocument, document: insertedDocument, ...callbackProperties }});
document = await Connectors.get(collection, document._id);
/*
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
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');
debug(newDocument);
debugGroupEnd();
debug(`--------------- end \x1b[36m${collectionName} Create Mutator\x1b[0m ---------------`);
debug('');
endDebugMutator(collectionName, 'Create', { document });
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 schema = collection.simpleSchema()._schema;
// OpenCRUD backwards compatibility
selector = selector || { _id: documentId };
data = data || modifierToData({ $set: set, $unset: unset });
startDebugMutator(collectionName, 'Update', { selector, data });
if (isEmpty(selector)) {
throw new Error('Selector cannot be empty');
}
// 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)}`);
}
const callbackProperties = { data, document, currentUser, collection, context };
debug('');
debugGroup(`--------------- start \x1b[36m${collectionName} Update Mutator\x1b[0m ---------------`);
debug('// collectionName: ', collectionName);
debug('// selector: ', selector);
debug('// data: ', data);
// get a "preview" of the new document
let document = { ...oldDocument, ...data };
document = pickBy(document, f => f !== null);
/*
Properties
*/
const properties = { data, oldDocument, document, currentUser, collection, context };
/*
Validation
*/
if (validate) {
let validationErrors = [];
validationErrors = validationErrors.concat(validateData(data, document, collection, context));
validationErrors = await runCallbacks({ name: `${typeName.toLowerCase()}.update.validate`, iterator: validationErrors, properties: callbackProperties });
validationErrors = await runCallbacks({ name: '*.update.validate', iterator: validationErrors, properties: callbackProperties });
validationErrors = validationErrors.concat(validateData(data, document, collection, context));
validationErrors = await runCallbacks({ name: `${typeName.toLowerCase()}.update.validate`, iterator: validationErrors, properties });
validationErrors = await runCallbacks({ name: '*.update.validate', iterator: validationErrors, properties });
// OpenCRUD backwards compatibility
data = modifierToData(await runCallbacks(`${collectionName.toLowerCase()}.edit.validate`, dataToModifier(data), document, currentUser, validationErrors));
if (validationErrors.length) {
// eslint-disable-next-line no-console
console.log('// validationErrors');
// eslint-disable-next-line no-console
console.log(validationErrors);
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 } });
}
}
// get a "preview" of the new document
let newDocument = { ...document, ...data};
newDocument = pickBy(newDocument, f => f !== null);
/*
// run onUpdate step
for(let fieldName of Object.keys(schema)) {
onUpdate
*/
for (let fieldName of Object.keys(schema)) {
let autoValue;
if (schema[fieldName].onUpdate) {
// eslint-disable-next-line no-await-in-loop
autoValue = await schema[fieldName].onUpdate({ data: clone(data), document, currentUser, newDocument, context });
autoValue = await schema[fieldName].onUpdate(properties); // eslint-disable-line no-await-in-loop
} else if (schema[fieldName].onEdit) {
// OpenCRUD backwards compatibility
// eslint-disable-next-line no-await-in-loop
autoValue = await schema[fieldName].onEdit(dataToModifier(clone(data)), document, currentUser, newDocument);
autoValue = await schema[fieldName].onEdit(dataToModifier(clone(data)), document, currentUser, document); // eslint-disable-line no-await-in-loop
}
if (typeof autoValue !== 'undefined') {
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
data = modifierToData(await runCallbacks(`${collectionName.toLowerCase()}.edit.before`, dataToModifier(data), document, currentUser, newDocument));
data = modifierToData(await runCallbacks(`${collectionName.toLowerCase()}.edit.sync`, 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, document));
// update connector requires a modifier, so get it from data
const modifier = dataToModifier(data);
// remove empty modifiers
if (_.isEmpty(modifier.$set)) {
if (isEmpty(modifier.$set)) {
delete modifier.$set;
}
if (_.isEmpty(modifier.$unset)) {
if (isEmpty(modifier.$unset)) {
delete modifier.$unset;
}
if (!_.isEmpty(modifier)) {
/*
DB Operation
*/
if (!isEmpty(modifier)) {
// update document
await Connectors.update(collection, selector, modifier, { removeEmptyStrings: false });
// 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
// 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
await runCallbacksAsync({ name: `${typeName.toLowerCase()}.update.async`, properties: { ...callbackProperties,newDocument, document: newDocument, oldDocument: document }});
await runCallbacksAsync({ name: '*.update.async', properties: { ...callbackProperties, newDocument, document: newDocument, oldDocument: document }});
await runCallbacksAsync({ name: `${typeName.toLowerCase()}.update.async`, properties });
await runCallbacksAsync({ name: '*.update.async', properties });
// 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');
debug('// modifier: ', modifier);
debugGroupEnd();
debug(`--------------- end \x1b[36m${collectionName} Update Mutator\x1b[0m ---------------`);
debug('');
endDebugMutator(collectionName, 'Update', { modifier });
return { data: newDocument };
return { data: document };
};
/*
Delete
*/
export const deleteMutator = async ({ collection, documentId, selector, currentUser, validate, context, document }) => {
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;
// OpenCRUD backwards compatibility
selector = selector || { _id: documentId };
@ -290,51 +335,68 @@ export const deleteMutator = async ({ collection, documentId, selector, currentU
throw new Error('Selector cannot be empty');
}
document = document || await Connectors.get(collection, selector);
document = document || (await Connectors.get(collection, selector));
if (!document) {
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) {
let validationErrors = [];
validationErrors = await runCallbacks({ name: `${typeName.toLowerCase()}.delete.validate`, iterator: validationErrors, properties: callbackProperties });
validationErrors = await runCallbacks({ name: '*.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 });
// OpenCRUD backwards compatibility
document = await runCallbacks(`${collectionName.toLowerCase()}.remove.validate`, document, currentUser);
if (validationErrors.length) {
// eslint-disable-next-line no-console
console.log('// validationErrors');
// eslint-disable-next-line no-console
console.log(validationErrors);
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 } });
}
}
// run onRemove step
for(let fieldName of Object.keys(schema)) {
/*
onDelete
*/
for (let fieldName of Object.keys(schema)) {
if (schema[fieldName].onDelete) {
// eslint-disable-next-line no-await-in-loop
await schema[fieldName].onDelete({ document, currentUser, context });
await schema[fieldName].onDelete(properties); // eslint-disable-line no-await-in-loop
} else if (schema[fieldName].onRemove) {
// OpenCRUD backwards compatibility
// eslint-disable-next-line no-await-in-loop
await schema[fieldName].onRemove(document, currentUser);
await schema[fieldName].onRemove(document, currentUser); // eslint-disable-line no-await-in-loop
}
}
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
await runCallbacks(`${collectionName.toLowerCase()}.remove.before`, document, currentUser);
await runCallbacks(`${collectionName.toLowerCase()}.remove.sync`, document, currentUser);
/*
DB Operation
*/
await Connectors.delete(collection, selector);
// 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);
}
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
await runCallbacksAsync(`${collectionName.toLowerCase()}.remove.async`, document, currentUser, collection);
debugGroupEnd();
debug(`--------------- end \x1b[36m${collectionName} Delete Mutator\x1b[0m ---------------`);
debug('');
endDebugMutator(collectionName, 'Delete');
return { data: document };
};
@ -362,3 +427,20 @@ export const removeMutation = deleteMutator;
export const newMutator = createMutator;
export const editMutator = updateMutator;
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 { Checkbox } from 'formsy-react-components';
import { registerComponent } from 'meteor/vulcan:core';
import Form from 'react-bootstrap/lib/Form';
import { Components, registerComponent } from 'meteor/vulcan:core';
const CheckboxComponent = ({refFunction, inputProperties}) =>
<Checkbox {...inputProperties} ref={refFunction} />;
const CheckboxComponent = ({ refFunction, path, inputProperties, itemProperties }) => (
<Components.FormItem {...inputProperties} {...itemProperties}>
<Form.Check {...inputProperties} id={path} ref={refFunction} checked={!!inputProperties.value}/>
</Components.FormItem>
);
registerComponent('FormComponentCheckbox', CheckboxComponent);

View file

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

View file

@ -1,43 +1,34 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import DateTimePicker from 'react-datetime';
import { registerComponent } from 'meteor/vulcan:core';
import { Components, registerComponent } from 'meteor/vulcan:core';
class DateComponent extends PureComponent {
constructor(props) {
super(props);
this.updateDate = this.updateDate.bind(this);
}
// when the datetime picker has mounted, SmartForm will catch the date value (no formsy mixin in this component)
// componentDidMount() {
// if (this.props.value) {
// this.updateDate(this.props.value);
// }
// }
updateDate(date) {
this.context.updateCurrentValues({[this.props.path]: date});
this.context.updateCurrentValues({ [this.props.path]: date });
}
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 (
<div className="form-group row">
<label className="control-label col-sm-3">{this.props.label}</label>
<div className="col-sm-9">
<DateTimePicker
value={date}
timeFormat={false}
// newDate argument is a Moment object given by react-datetime
onChange={newDate => this.updateDate(newDate)}
inputProps={{name: this.props.name}}
/>
</div>
</div>
<Components.FormItem {...this.props.inputProperties} {...this.props.itemProperties}>
<DateTimePicker
value={date}
timeFormat={false}
// newDate argument is a Moment object given by react-datetime
onChange={newDate => this.updateDate(newDate)}
inputProps={{ name: this.props.name }}
/>
</Components.FormItem>
);
}
}
@ -57,4 +48,4 @@ DateComponent.contextTypes = {
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 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 {
state = {
year: null,
month: null,
day: null,
}
};
updateDate = (date) => {
updateDate = date => {
const { value, path } = this.props;
let newDate;
this.setState(date, () => {
@ -20,7 +23,10 @@ class DateComponent2 extends PureComponent {
if (isEmptyValue(value)) {
if (year && month && day) {
// 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() });
}
} else {
@ -32,10 +38,9 @@ class DateComponent2 extends PureComponent {
this.props.updateCurrentValues({ [path]: newDate.toDate() });
}
});
}
};
render() {
const { path, value } = this.props;
const months = moment.months();
const mDate = !isEmptyValue(value) && moment(value);
@ -45,10 +50,10 @@ class DateComponent2 extends PureComponent {
name: `${path}.month`,
layout: 'vertical',
options: months.map((m, i) => ({ label: m, value: m })),
value: mDate && mDate.format('MMMM') || '',
value: (mDate && mDate.format('MMMM')) || '',
onChange: (name, value) => {
this.updateDate({ month: value });
}
},
};
const dayProperties = {
@ -56,10 +61,10 @@ class DateComponent2 extends PureComponent {
name: `${path}.day`,
layout: 'vertical',
maxLength: 2,
value: mDate && mDate.format('DD') || '',
onBlur: (e) => {
value: (mDate && mDate.format('DD')) || '',
onBlur: e => {
this.updateDate({ day: e.target.value });
}
},
};
const yearProperties = {
@ -67,25 +72,33 @@ class DateComponent2 extends PureComponent {
name: `${path}.year`,
layout: 'vertical',
maxLength: 4,
value: mDate && mDate.format('YYYY') || '',
onBlur: (e) => {
value: (mDate && mDate.format('YYYY')) || '',
onBlur: e => {
this.updateDate({ year: e.target.value });
}
},
};
return (
<div className="form-group row">
<label className="control-label col-sm-3">{this.props.label}</label>
<div className="col-sm-9" style={{ display: 'flex', alignItems: 'center' }}>
<div><Components.FormComponentSelect inputProperties={monthProperties} 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>
<Components.FormItem {...this.props.inputProperties} {...this.props.itemProperties}>
<div>
<div>
<Components.FormComponentSelect
inputProperties={monthProperties}
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>
</Components.FormItem>
);
}
}
export default DateComponent2;
registerComponent('FormComponentDate2', DateComponent2);
registerComponent('FormComponentDate2', DateComponent2);

View file

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

View file

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

View file

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

View file

@ -21,15 +21,20 @@ class FormComponentInner extends PureComponent {
};
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
// and avoid https://facebook.github.io/react/warnings/unknown-prop.html warnings
const inputProperties = {
name,
path,
options,
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,
disabled,
...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 { Input } from 'formsy-react-components';
import { registerComponent } from 'meteor/vulcan:core';
import Form from 'react-bootstrap/lib/Form';
import { Components, registerComponent } from 'meteor/vulcan:core';
const NumberComponent = ({refFunction, inputProperties}) =>
<Input {...inputProperties} ref={refFunction} type="number" />;
const NumberComponent = ({ refFunction, inputProperties, itemProperties }) => (
<Components.FormItem {...inputProperties} {...itemProperties}>
<Form.Control {...inputProperties} ref={refFunction} type="number" />
</Components.FormItem>
);
registerComponent('FormComponentNumber', NumberComponent);

View file

@ -1,7 +1,11 @@
import React from 'react';
import { RadioGroup } from 'formsy-react-components';
import { registerComponent } from 'meteor/vulcan:core';
import Form from 'react-bootstrap/lib/Form';
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 { intlShape } from 'meteor/vulcan:i18n';
import { Select } from 'formsy-react-components';
import { registerComponent } from 'meteor/vulcan:core';
import Form from 'react-bootstrap/lib/Form';
import { Components, registerComponent } from 'meteor/vulcan:core';
// copied from vulcan:forms/utils.js to avoid extra dependency
const getFieldType = datatype => datatype && datatype[0].type;
const SelectComponent = ({refFunction, inputProperties, datatype, ...properties}, { intl }) => {
const SelectComponent = ({ refFunction, inputProperties, itemProperties, datatype }, { intl }) => {
const noneOption = {
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
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];
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 = {

View file

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

View file

@ -1,15 +1,20 @@
import React from 'react';
import { registerComponent } from 'meteor/vulcan:core';
import { Components, registerComponent } from 'meteor/vulcan:core';
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 }) => (
<div className="form-group row">
<label className="control-label col-sm-3">{label}</label>
<div className="col-sm-9">{parseUrl(value)}</div>
</div>
const StaticComponent = ({ inputProperties, itemProperties }) => (
<Components.FormItem {...inputProperties} {...itemProperties}>
<div>{parseUrl(inputProperties.value)}</div>
</Components.FormItem>
);
registerComponent('FormComponentStaticText', StaticComponent);

View file

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

View file

@ -1,7 +1,11 @@
import React from 'react';
import { Input } from 'formsy-react-components';
import { registerComponent } from 'meteor/vulcan:core';
import Form from 'react-bootstrap/lib/Form';
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/Checkboxgroup.jsx';
import '../components/forms/Date.jsx';
@ -15,6 +16,7 @@ import '../components/forms/Url.jsx';
import '../components/forms/StaticText.jsx';
import '../components/forms/FormComponentInner.jsx';
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/Alert.jsx';