material-ui

This commit is contained in:
eric-burel 2019-03-15 09:34:30 +01:00
parent 448f9c16b8
commit a743022545
90 changed files with 7582 additions and 22 deletions

View file

@ -55,7 +55,7 @@
"avoid-escape"
],
"react/prop-types": 0,
"semi": [1, "always"]
"semi": [2, "always"]
},
"env": {
"browser": true,

64
package-lock.json generated
View file

@ -1,6 +1,6 @@
{
"name": "Vulcan",
"version": "1.12.16",
"version": "1.12.17",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -702,7 +702,6 @@
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
"integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=",
"optional": true,
"requires": {
"kind-of": "^3.0.2",
"longest": "^1.0.1",
@ -898,11 +897,12 @@
}
},
"apollo-errors": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/apollo-errors/-/apollo-errors-1.5.1.tgz",
"integrity": "sha512-gYAceMzNJfF+mUHH2/4UcZTkZtDY54arCTKGbKa7WU5IXnTJ4V+P94wHodcDkLLHWpHL8SW1hEgjN5ZINcPb1w==",
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/apollo-errors/-/apollo-errors-1.9.0.tgz",
"integrity": "sha512-XVukHd0KLvgY6tNjsPS3/Re3U6RQlTKrTbIpqqeTMo2N34uQMr+H1UheV21o8hOZBAFosvBORVricJiP5vfmrw==",
"requires": {
"es6-error": "^4.0.0"
"assert": "^1.4.1",
"extendable-error": "^0.1.5"
}
},
"apollo-link": {
@ -1289,6 +1289,14 @@
"safer-buffer": "~2.1.0"
}
},
"assert": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz",
"integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=",
"requires": {
"util": "0.10.3"
}
},
"assert-err": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/assert-err/-/assert-err-1.1.0.tgz",
@ -2384,7 +2392,7 @@
},
"cheerio": {
"version": "0.22.0",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz",
"resolved": "http://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz",
"integrity": "sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=",
"requires": {
"css-select": "~1.2.0",
@ -2831,7 +2839,7 @@
},
"css-select": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
"resolved": "http://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
"integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=",
"requires": {
"boolbase": "~1.0.0",
@ -3370,11 +3378,6 @@
"es6-symbol": "~3.1.1"
}
},
"es6-error": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="
},
"es6-iterator": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
@ -3977,6 +3980,11 @@
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
"integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ="
},
"extendable-error": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.5.tgz",
"integrity": "sha1-EiMIpwl7yJomOyxPvwiceBQOO20="
},
"extglob": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz",
@ -6168,8 +6176,7 @@
"longest": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
"integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=",
"optional": true
"integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc="
},
"loose-envify": {
"version": "1.3.1",
@ -6261,7 +6268,7 @@
"process": "^0.11.9",
"punycode": "^1.4.1",
"querystring-es3": "^0.2.1",
"readable-stream": "git+https://github.com/meteor/readable-stream.git",
"readable-stream": "git+https://github.com/meteor/readable-stream.git#c688cdd193549919b840e8d72a86682d91961e12",
"stream-browserify": "^2.0.1",
"string_decoder": "^1.0.1",
"timers-browserify": "^1.4.2",
@ -6711,11 +6718,11 @@
"version": "git+https://github.com/meteor/readable-stream.git#c688cdd193549919b840e8d72a86682d91961e12",
"from": "git+https://github.com/meteor/readable-stream.git",
"requires": {
"inherits": "~2.0.3",
"inherits": "~2.0.1",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.0",
"process-nextick-args": "~1.0.6",
"safe-buffer": "^5.0.1",
"string_decoder": "~1.0.0",
"util-deprecate": "~1.0.1"
}
},
@ -9717,6 +9724,21 @@
"os-homedir": "^1.0.0"
}
},
"util": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
"requires": {
"inherits": "2.0.1"
},
"dependencies": {
"inherits": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
"integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE="
}
}
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -9852,7 +9874,7 @@
},
"chalk": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"requires": {
"ansi-styles": "^2.2.1",

View file

@ -0,0 +1,15 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.js]
trim_trailing_whitespace = false
[*.md]
trim_trailing_whitespace = false

View file

@ -0,0 +1,86 @@
{
"extends": [
"eslint:recommended",
//"airbnb",
"plugin:meteor/recommended",
"plugin:react/recommended"
],
"parser": "babel-eslint",
"parserOptions": {
"allowImportExportEverywhere": true,
"ecmaVersion": 6,
"sourceType": "module"
},
"rules": {
"babel/generator-star-spacing": 0,
"babel/new-cap": [1, {
"capIsNewExceptions": [
"Optional",
"OneOf",
"Maybe",
"MailChimpAPI",
"Juice",
"Run",
"AppComposer",
"Query",
"InArray"
]
}],
"babel/array-bracket-spacing": 0,
"babel/object-curly-spacing": 0,
"babel/object-shorthand": 0,
"babel/arrow-parens": 0,
"babel/no-await-in-loop": 1,
"comma-dangle": 0,
"key-spacing": 0,
"no-extra-boolean-cast": 0,
"no-undef": 1,
"no-unused-vars": [1, {
"vars": "all",
"args": "none",
"varsIgnorePattern": "React|PropTypes|Component"
}],
"react/prop-types": 0,
"react/display-name": 0,
"meteor/audit-argument-checks": 0,
"meteor/no-session": 0,
"no-case-declarations": 0,
"no-console": 0,
"semi": "error",
"quotes": [
1,
"single"
]
},
"env": {
"browser": true,
"commonjs": true,
"es6": true,
"meteor": true,
"node": true
},
"plugins": [
"babel",
"meteor",
"react",
"jsx-a11y"
],
"settings": {
"import/resolver": {
"meteor": {
"paths": [
"/usr/local/share/global_modules"
],
"moduleDirectory": [
"node_modules",
"packages"
]
}
}
},
"root": true,
"globals": {
"param": true,
"returns": true
}
}

View file

@ -0,0 +1,3 @@
npm-debug.log
node_modules
.idea/workspace.xml

View file

@ -0,0 +1,89 @@
accounts-base@1.4.3
allow-deny@1.1.0
autoupdate@1.5.0
babel-compiler@7.2.4
babel-runtime@1.3.0
base64@1.0.11
binary-heap@1.0.11
blaze-tools@1.0.10
boilerplate-generator@1.6.0
buffer@0.0.0
caching-compiler@1.2.1
caching-html-compiler@1.1.3
callback-hook@1.1.0
check@1.3.1
ddp@1.4.0
ddp-client@2.3.3
ddp-common@1.4.0
ddp-rate-limiter@1.0.7
ddp-server@2.2.0
deps@1.0.12
diff-sequence@1.1.1
dynamic-import@0.5.0
ecmascript@0.12.4
ecmascript-runtime@0.7.0
ecmascript-runtime-client@0.8.0
ecmascript-runtime-server@0.7.1
ejson@1.1.0
email@1.2.3
erikdakoda:vulcan-material-ui@1.12.8_17
es5-shim@4.8.0
fetch@0.1.0
fourseven:scss@4.10.0
geojson-utils@1.0.10
hot-code-push@1.0.4
html-tools@1.0.11
htmljs@1.0.11
http@1.4.1
id-map@1.1.0
inter-process-messaging@0.1.0
localstorage@1.2.0
logging@1.1.20
meteor@1.9.2
meteorhacks:inject-initial@1.0.4
meteorhacks:picker@1.0.3
minifier-css@1.4.1
minifier-js@2.4.0
minimongo@1.4.5
modern-browsers@0.1.3
modules@0.13.0
modules-runtime@0.10.3
mongo@1.6.0
mongo-decimal@0.1.0
mongo-dev-server@1.1.0
mongo-id@1.0.7
npm-mongo@3.1.1
ordered-dict@1.1.0
percolatestudio:synced-cron@1.1.0
promise@0.11.2
random@1.1.0
rate-limit@1.0.9
reactive-dict@1.2.1
reactive-var@1.0.11
reload@1.2.0
retry@1.1.0
routepolicy@1.1.0
server-render@0.3.1
service-configuration@1.0.11
session@1.2.0
shell-server@0.4.0
socket-stream-client@0.2.2
spacebars-compiler@1.1.3
standard-minifier-css@1.5.2
standard-minifier-js@2.4.0
static-html@1.2.2
templating-tools@1.1.2
tracker@1.2.0
underscore@1.0.10
url@1.2.0
vulcan:accounts@1.12.8
vulcan:core@1.12.8
vulcan:debug@1.12.8
vulcan:email@1.12.8
vulcan:forms@1.12.8
vulcan:i18n@1.12.8
vulcan:lib@1.12.8
vulcan:routing@1.12.8
vulcan:users@1.12.8
webapp@1.7.1
webapp-hashing@1.0.9

View file

@ -0,0 +1,18 @@
.accounts-ui .form-control {
color: rgba(0, 0, 0, 0.87);
padding: 8px 0;
font-size: 1rem;
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
border: none;
box-shadow: none;
border-bottom: 1px solid rgba(0, 0, 0, 0.87);
margin-bottom: 1px;
border-radius: 0;
width: 100%;
}
.accounts-ui .form-control:focus {
border-bottom-width: 2px;
margin-bottom: 0;
}

View file

@ -0,0 +1,3 @@
export * from '../components/index';
export * from '../modules/index';
import './wrapWithMuiTheme';

View file

@ -0,0 +1,29 @@
import React from 'react';
import { addCallback, registerComponent, Components } from 'meteor/vulcan:core';
import MuiThemeProvider from '@material-ui/core/styles/MuiThemeProvider';
import { getCurrentTheme } from '../modules/themes';
import JssCleanup from '../components/theme/JssCleanup';
class ThemeProvider extends React.Component {
render() {
const theme = getCurrentTheme();
return (
<MuiThemeProvider theme={theme}>
<JssCleanup>{this.props.children}</JssCleanup>
</MuiThemeProvider>
);
}
}
registerComponent('ThemeProvider', ThemeProvider);
function wrapWithMuiTheme(app) {
return (
<Components.ThemeProvider>
{app}
</Components.ThemeProvider>
);
}
addCallback('router.client.wrapper', wrapWithMuiTheme);

View file

@ -0,0 +1,46 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Button from '@material-ui/core/Button';
import { replaceComponent, Utils } from 'meteor/vulcan:core';
import classNames from 'classnames';
export class AccountsButton extends Component {
render () {
const {
label,
type,
disabled = false,
className,
onClick
} = this.props;
return (
<Button
variant={type === 'link' ? 'text' : 'contained'}
size={type === 'link' ? 'small' : undefined}
color="primary"
className={classNames(`button-${Utils.slugify(label)}`, className)}
type={type}
disabled={disabled}
onClick={onClick}
disableRipple={true}
>
{label}
</Button>
);
}
}
AccountsButton.propTypes = {
label: PropTypes.string.isRequired,
type: PropTypes.oneOf(['link', 'submit', 'button']),
disabled: PropTypes.bool,
className: PropTypes.string,
onClick: PropTypes.func.isRequired,
};
replaceComponent('AccountsButton', AccountsButton);

View file

@ -0,0 +1,48 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Components, replaceComponent } from 'meteor/vulcan:core';
import CardActions from '@material-ui/core/CardActions';
import withStyles from '@material-ui/core/styles/withStyles';
import classNames from 'classnames';
const styles = theme => ({
root: {
flexDirection: 'row-reverse',
padding: theme.spacing.unit * 2,
height: 'auto',
},
});
export class AccountsButtons extends Component {
render () {
const {
classes,
buttons = {},
className = 'buttons',
} = this.props;
return (
<CardActions className={classNames(classes.root, className)}>
{Object.keys(buttons).map((id, i) =>
<Components.AccountsButton {...buttons[id]} key={i}/>
)}
</CardActions>
);
}
}
AccountsButtons.propTypes = {
classes: PropTypes.object.isRequired,
buttons: PropTypes.object,
className: PropTypes.string,
};
AccountsButtons.displayName = 'AccountsButtons';
replaceComponent('AccountsButtons', AccountsButtons, [withStyles, styles]);

View file

@ -0,0 +1,114 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { replaceComponent } from 'meteor/vulcan:core';
import TextField from '@material-ui/core/TextField';
const autocompleteValues = {
'username': 'username',
'usernameOrEmail': 'email',
'email': 'email',
'password': 'current-password'
};
export class AccountsField extends PureComponent {
constructor (props) {
super(props);
this.state = {
mount: true
};
}
triggerUpdate () {
// Trigger an onChange on initial load, to support browser pre-filled values.
const { onChange } = this.props;
if (this.input && onChange) {
onChange({ target: { value: this.input.value } });
}
}
componentDidMount () {
this.triggerUpdate();
}
componentDidUpdate (prevProps) {
// Re-mount component so that we don't expose browser pre-filled passwords if the component was
// a password before and now something else.
if (prevProps.id !== this.props.id) {
this.setState({ mount: false });
} else if (!this.state.mount) {
this.setState({ mount: true });
this.triggerUpdate();
}
}
render () {
const {
id,
hint,
label,
type = 'text',
onChange,
required = false,
className = 'field',
defaultValue = '',
autoFocus,
messages,
} = this.props;
let { message } = this.props;
const { mount = true } = this.state;
if (type === 'notice') {
return <div className={className}>{label}</div>;
}
const autoComplete = autocompleteValues[id];
if (messages && messages.find && typeof id === 'string') {
const foundMessage = messages.find(element => {
if (typeof element.field !== 'string') return false;
return id.toLowerCase().indexOf(element.field.toLowerCase()) > -1;
});
if (foundMessage) {
message = foundMessage;
}
}
return (
mount &&
<div className={className} style={{ marginBottom: '10px' }}>
<TextField
id={id}
type={type}
inputRef={ref => { this.input = ref; }}
onChange={onChange}
placeholder={hint}
defaultValue={defaultValue}
autoComplete={autoComplete }
label={label}
autoFocus={autoFocus}
required={required}
error={!!message}
helperText={message && message.message}
fullWidth
/>
</div>
);
}
}
AccountsField.propTypes = {
onChange: PropTypes.func,
};
replaceComponent('AccountsField', AccountsField);

View file

@ -0,0 +1,27 @@
import React, { Component } from 'react';
import { Components, replaceComponent } from 'meteor/vulcan:core';
import CardContent from '@material-ui/core/CardContent';
export class AccountsFields extends Component {
render () {
const {
fields = {},
className = 'fields',
messages,
} = this.props;
return (
<CardContent className={className}>
{
Object.keys(fields).map((id, i) =>
<Components.AccountsField {...fields[id]} messages={messages} autoFocus={i === 0} key={i}/>
)
}
</CardContent>
);
}
}
replaceComponent('AccountsFields', AccountsFields);

View file

@ -0,0 +1,68 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Components, registerComponent } from 'meteor/vulcan:core';
import withStyles from '@material-ui/core/styles/withStyles';
const styles = theme => ({
messages: theme.utils.errorMessage,
});
export class AccountsForm extends Component {
componentDidMount () {
let form = this.form;
if (form) {
form.addEventListener('submit', (e) => {
e.preventDefault();
});
}
}
render () {
const {
oauthServices,
fields,
buttons,
messages,
ready = true,
className,
classes,
} = this.props;
return (
<form ref={(ref) => this.form = ref}
className={classNames(className, 'accounts-ui', { 'ready': ready, })}
noValidate
>
<Components.AccountsFields fields={fields} messages={messages}/>
<Components.AccountsButtons buttons={{...buttons}}/>
<Components.AccountsPasswordOrService oauthServices={oauthServices}/>
<Components.AccountsSocialButtons oauthServices={oauthServices}/>
<Components.AccountsFormMessages messages={messages} className={classes.messages}/>
</form>
);
}
}
AccountsForm.propTypes = {
oauthServices: PropTypes.object,
fields: PropTypes.object.isRequired,
buttons: PropTypes.object.isRequired,
error: PropTypes.string,
ready: PropTypes.bool,
classes: PropTypes.object.isRequired,
};
AccountsForm.displayName = 'AccountsForm';
registerComponent('AccountsForm', AccountsForm, [withStyles, styles]);

View file

@ -0,0 +1,57 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { replaceComponent } from 'meteor/vulcan:core';
import { intlShape } from 'meteor/vulcan:i18n';
import Typography from '@material-ui/core/Typography';
import CardActions from '@material-ui/core/CardActions';
import withStyles from '@material-ui/core/styles/withStyles';
import classNames from 'classnames';
const styles = theme => ({
root: {
flexDirection: 'row-reverse',
paddingRight: theme.spacing.unit * 2,
paddingLeft: theme.spacing.unit * 2,
height: 'auto',
},
typography: {
marginRight: theme.spacing.unit,
}
});
export function hasPasswordService() {
// First look for OAuth services.
return !!Package['accounts-password'];
}
export class AccountsPasswordOrService extends PureComponent {
render () {
let { className = 'password-or-service', style = {}, classes } = this.props;
const services = Object.keys(this.props.oauthServices).map(service => {
return this.props.oauthServices[service].label;
});
let labels = services;
if (services.length > 2) {
labels = [];
}
if (hasPasswordService() && services.length > 0) {
return (<CardActions className={classNames(className, classes.root)}>
<Typography variant="caption" className={classes.typography} align="right">
{ `${this.context.intl.formatMessage({id: 'accounts.or_use'})} ${ labels.join(' / ') }` }
</Typography></CardActions>
);
}
return null;
}
}
AccountsPasswordOrService.propTypes = {
oauthServices: PropTypes.object
};
AccountsPasswordOrService.contextTypes = {
intl: intlShape
};
replaceComponent('AccountsPasswordOrService', AccountsPasswordOrService, [withStyles, styles]);

View file

@ -0,0 +1,28 @@
import React from 'react';
import { Components, replaceComponent } from 'meteor/vulcan:core';
import CardActions from '@material-ui/core/CardActions'
import withStyles from '@material-ui/core/styles/withStyles'
import classNames from 'classnames'
const styles = theme => ({
root: {
justifyContent: 'flex-end',
padding: theme.spacing.unit * 2,
height: 'auto',
},
});
export class AccountsSocialButtons extends React.Component {
render() {
let { oauthServices = {}, className = 'social-buttons', classes } = this.props;
return(
<CardActions className={classNames(classes.root, className)}>
{Object.keys(oauthServices).map((id, i) => {
return <Components.AccountsButton {...oauthServices[id]} key={i} />;
})}
</CardActions>
);
}
}
replaceComponent('AccountsSocialButtons', AccountsSocialButtons, [withStyles, styles]);

View file

@ -0,0 +1,133 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, intlShape } from 'react-intl';
import { Components, registerComponent } from 'meteor/vulcan:core';
import withStyles from '@material-ui/core/styles/withStyles';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import ArrowDownIcon from 'mdi-material-ui/ArrowDown';
import ScrollTrigger from './ScrollTrigger';
import classNames from 'classnames';
const styles = theme => ({
root: {
textAlign: 'center',
flexBasis: '100%',
},
textButton: {
marginTop: theme.spacing.unit * 2,
},
iconButton: {},
caption: {
marginTop: theme.spacing.unit * 3,
paddingTop: theme.spacing.unit,
paddingBottom: theme.spacing.unit,
},
});
const LoadMore = ({
classes,
count,
totalCount,
loadMore,
networkStatus,
showCount,
useTextButton,
className,
infiniteScroll,
}, { intl }) => {
const isLoadingMore = networkStatus === 2;
const loadMoreText = intl.formatMessage({ id: 'load_more.load_more' });
const title = `${loadMoreText} (${count}/${totalCount})`;
const hasMore = totalCount > count;
const countValues = { count, totalCount };
const loadMoreButton = useTextButton
?
<Button className={classes.textButton} onClick={() => loadMore()}>
{title}
</Button>
:
<IconButton className={classes.iconButton} onClick={() => loadMore()}>
<ArrowDownIcon/>
</IconButton>;
return (
<div className={classNames('load-more', classes.root, className)}>
{
showCount &&
<Typography variant="caption" className={classes.caption}>
<FormattedMessage id={`load_more.${hasMore ? 'loaded_count' : 'loaded_all'}`} values={countValues}/>
</Typography>
}
{
isLoadingMore
?
<Components.Loading/>
:
hasMore
?
infiniteScroll
?
<ScrollTrigger onEnter={() => loadMore()}>
{loadMoreButton}
</ScrollTrigger>
:
loadMoreButton
:
null
}
</div>
);
};
LoadMore.propTypes = {
classes: PropTypes.object.isRequired,
count: PropTypes.number,
totalCount: PropTypes.number,
loadMore: PropTypes.func,
networkStatus: PropTypes.number,
showCount: PropTypes.bool,
useTextButton: PropTypes.bool,
className: PropTypes.string,
infiniteScroll: PropTypes.bool,
};
LoadMore.defaultProps = {
showCount: true,
};
LoadMore.contextTypes = {
intl: intlShape.isRequired,
};
LoadMore.displayName = 'LoadMore';
registerComponent('LoadMore', LoadMore, [withStyles, styles]);

View file

@ -0,0 +1,125 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import _throttle from 'lodash/throttle';
class ScrollTrigger extends Component {
constructor (props) {
super(props);
this.onScroll = _throttle(this.onScroll.bind(this), 100, {
leading: true,
trailing: true,
});
this.onResize = _throttle(this.onResize.bind(this), 100, {
leading: true,
trailing: true,
});
this.inViewport = false;
this.passive = this.supportsPassive ? { passive: true } : false;
}
supportsPassive () {
let supportsPassive = false;
try {
const opts = Object.defineProperty({}, 'passive', {
get: function() {
supportsPassive = true;
}
});
window.addEventListener("testPassive", null, opts);
window.removeEventListener("testPassive", null, opts);
//eslint-disable-next-line no-empty
} catch (e) {}
return supportsPassive;
}
componentDidMount () {
this.scroller = document.getElementById('main');
this.scroller.addEventListener('resize', this.onResize, this.passive);
this.scroller.addEventListener('scroll', this.onScroll, this.passive);
this.inViewport = false;
if (this.props.triggerOnLoad) {
this.checkStatus();
}
}
componentWillUnmount () {
if (!this.scroller) return;
this.scroller.removeEventListener('resize', this.onResize);
this.scroller.removeEventListener('scroll', this.onScroll);
this.scroller = null;
}
onResize () {
this.checkStatus();
}
onScroll () {
this.checkStatus();
}
checkStatus () {
if (!this.scroller) return;
const {
onEnter,
} = this.props;
//eslint-disable-next-line
const element = ReactDOM.findDOMNode(this.element);
const elementRect = element.getBoundingClientRect();
const viewportEnd = this.scroller.clientHeight + this.props.preload;
const inViewport = elementRect.top < viewportEnd;
if (inViewport) {
if (!this.inViewport) {
this.inViewport = true;
onEnter(this);
}
} else {
if (this.inViewport) {
this.inViewport = false;
}
}
}
render () {
const {
children,
} = this.props;
return (
<div ref={(element) => {this.element = element;}}>
{children}
</div>
);
}
}
ScrollTrigger.propTypes = {
scrollerId: PropTypes.string,
triggerOnLoad: PropTypes.bool,
preload: PropTypes.number,
onEnter: PropTypes.func,
};
ScrollTrigger.defaultProps = {
scrollerId: 'main',
preload: 1000,
triggerOnLoad: true,
onEnter: () => {},
};
export default ScrollTrigger;

View file

@ -0,0 +1,240 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { Components, registerComponent } from 'meteor/vulcan:core';
import withStyles from '@material-ui/core/styles/withStyles';
import SearchIcon from 'mdi-material-ui/Magnify';
import ClearIcon from 'mdi-material-ui/CloseCircle';
import Input from '@material-ui/core/Input';
import NoSsr from '@material-ui/core/NoSsr';
import classNames from 'classnames';
import _debounce from 'lodash/debounce';
import KeyboardEventHandler from 'react-keyboard-event-handler';
import autosizeInput from 'autosize-input';
const styles = theme => ({
'@global': {
'input[type=text]::-ms-clear, input[type=text]::-ms-reveal':
{
display: 'none',
width: 0,
height: 0,
},
'input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button':
{ display: 'none' },
'input[type="search"]::-webkit-search-results-button, input[type="search"]::-webkit-search-results-decoration':
{ display: 'none' },
},
root: {
display: 'inline-flex',
backgroundColor: theme.palette.common.faintBlack,
borderRadius: 20,
padding: 6,
},
clear: {
transition: theme.transitions.create('opacity,transform', {
duration: theme.transitions.duration.short,
}),
opacity: 0.65,
width: 36,
height: 36,
margin: -6,
marginLeft: 0,
'& svg': {
width: 16,
height: 16,
},
flexDirection: 'column',
},
clearDense: {
width: 32,
height: 32,
margin: -4,
marginLeft: 0,
},
clearDisabled: {
opacity: 0,
pointerEvents: 'none',
},
dense: {
padding: 4,
},
icon: {
color: theme.palette.common.lightBlack,
marginLeft: theme.spacing.unit,
marginRight: theme.spacing.unit,
},
input: {
lineHeight: 1,
paddingTop: 2,
paddingBottom: 2,
marginBottom: 1,
/*transition: theme.transitions.create('width', {
duration: theme.transitions.duration.shortest,
}),*/
minWidth: 130,
},
});
class SearchInput extends PureComponent {
constructor (props) {
super(props);
this.state = {
value: props.defaultValue || '',
};
this.input = null;
this.removeAutosize = null;
this.triggerResize = null;
this.updateQuery = _debounce(this.updateQuery, 500);
}
componentDidMount () {
if (!document) return;
const element = document.querySelector(`.search-input-${this.props.name} input[type=search]`);
// We have to patch into addEventListener because autosizeInput provides no way to trigger resize
element._addEventListener = element.addEventListener;
element.addEventListener = function(type, listener, useCapture) {
if(useCapture === undefined)
useCapture = false;
this._addEventListener(type, listener, useCapture);
this.triggerResize = listener;
};
this.removeAutosize = autosizeInput(element);
this.triggerResize = element.triggerResize;
element.addEventListener = element._addEventListener;
}
componentWillUnmount () {
if (this.removeAutosize) {
this.removeAutosize();
}
}
handleShortcutKeys = (key, event) => {
switch (key) {
case 's':
this.focusInput();
event.preventDefault();
break;
case 'c':
case 'esc':
this.clearSearch(event, true);
event.preventDefault();
break;
}
};
handleFocus = () => {
this.input.select();
};
focusInput = (event) => {
this.input.focus();
};
clearSearch = (event, dontFocus) => {
this.setState({ value: '' }, this.triggerResize);
this.updateQuery('');
if (!dontFocus) {
this.focusInput();
}
};
updateSearch = (event) => {
const value = event.target.value;
this.setState({ value: value });
this.updateQuery(value);
};
updateQuery = (value) => {
this.props.updateQuery(value);
};
render () {
const {
classes,
className,
dense,
noShortcuts,
name,
} = this.props;
const searchIcon = <SearchIcon className={classes.icon} onClick={this.focusInput}/>;
const clearButton = <Components.TooltipIntl
titleId="search.clear"
icon={<ClearIcon/>}
onClick={this.clearSearch}
classes={{
root: classNames(!this.state.value && classes.clearDisabled),
button: classNames('clear-button', classes.clear, dense && classes.clearDense),
}}
disabled={!this.state.value}
/>;
return (
<React.Fragment>
<Input className={classNames('search-input', `search-input-${name}`, classes.root, dense && classes.dense, className)}
classes={{ input: classes.input }}
id={`search-input-${name}`}
name={name}
inputRef={input => this.input = input}
value={this.state.value}
type="search"
onChange={this.updateSearch}
onFocus={this.handleFocus}
disableUnderline={true}
startAdornment={searchIcon}
endAdornment={clearButton}
/>
<NoSsr>
{
// KeyboardEventHandler is not valid on the server, where its name is undefined
typeof window !== 'undefined' && KeyboardEventHandler.name && !noShortcuts &&
<KeyboardEventHandler handleKeys={['s', 'c', 'esc']} onKeyEvent={this.handleShortcutKeys}/>
}
</NoSsr>
</React.Fragment>
);
}
}
SearchInput.propTypes = {
classes: PropTypes.object.isRequired,
updateQuery: PropTypes.func.isRequired,
className: PropTypes.string,
dense: PropTypes.bool,
noShortcuts: PropTypes.bool,
name: PropTypes.string.isRequired,
};
SearchInput.defaultProps = {
name: 'search',
};
SearchInput.displayName = 'SearchInput';
registerComponent('SearchInput', SearchInput, [withStyles, styles]);

View file

@ -0,0 +1,108 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Components, registerComponent, Utils } from 'meteor/vulcan:core';
import { intlShape } from 'meteor/vulcan:i18n';
import withStyles from '@material-ui/core/styles/withStyles';
import withTheme from '@material-ui/core/styles/withTheme';
import Tooltip from '@material-ui/core/Tooltip';
import IconButton from '@material-ui/core/IconButton';
import Button from '@material-ui/core/Button';
import classNames from 'classnames';
const styles = theme => ({
root: {},
tooltip: {
margin: '4px !important',
},
buttonWrap: {
display: 'inline-block',
},
button: {},
});
const TooltipIconButton = (props, { intl }) => {
const {
title,
titleId,
placement,
icon,
className,
classes,
theme,
buttonRef,
variant,
...properties
} = props;
const titleText = props.title || intl.formatMessage({ id: titleId });
const slug = Utils.slugify(titleId);
return (
<Tooltip classes={{ tooltip: classNames('tooltip-icon-button', classes.tooltip, className) }}
id={`tooltip-${slug}`}
title={titleText}
placement={placement}
enterDelay={theme.utils.tooltipEnterDelay}
>
<div className={classes.buttonWrap}>
{
variant === 'fab'
?
<Button className={classNames(classes.button, slug)}
variant="fab"
aria-label={title}
ref={buttonRef}
{...properties}
>
{icon}
</Button>
:
<IconButton className={classNames(classes.button, slug)}
aria-label={title}
ref={buttonRef}
{...properties}
>
{icon}
</IconButton>
}
</div>
</Tooltip>
);
};
TooltipIconButton.propTypes = {
title: PropTypes.node,
titleId: PropTypes.string,
placement: PropTypes.string,
icon: PropTypes.node.isRequired,
className: PropTypes.string,
classes: PropTypes.object,
buttonRef: PropTypes.func,
variant: PropTypes.string,
theme: PropTypes.object,
};
TooltipIconButton.defaultProps = {
placement: 'bottom',
};
TooltipIconButton.contextTypes = {
intl: intlShape.isRequired,
};
TooltipIconButton.displayName = 'TooltipIconButton';
registerComponent('TooltipIconButton', TooltipIconButton, [withStyles, styles], [withTheme]);

View file

@ -0,0 +1,168 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Components, registerComponent, Utils } from 'meteor/vulcan:core';
import { intlShape } from 'meteor/vulcan:i18n';
import withStyles from '@material-ui/core/styles/withStyles';
import withTheme from '@material-ui/core/styles/withTheme';
import Tooltip from '@material-ui/core/Tooltip';
import IconButton from '@material-ui/core/IconButton';
import Button from '@material-ui/core/Button';
import classNames from 'classnames';
const styles = theme => ({
root: {
display: 'inherit',
},
tooltip: {
margin: '4px !important',
},
buttonWrap: {
display: 'inherit',
},
button: {},
icon: {},
popoverPopper: {
zIndex: 1700,
},
popoverTooltip: {
zIndex: 1701,
},
});
const TooltipIntl = (props, { intl }) => {
const {
title,
titleId,
titleValues,
placement,
icon,
className,
classes,
theme,
enterDelay,
leaveDelay,
buttonRef,
variant,
parent,
children,
...properties
} = props;
const iconWithClass = icon && React.cloneElement(icon, { className: classes.icon });
const popperClass = parent === 'popover' && classes.popoverPopper;
const tooltipClass = parent === 'popover' && classes.popoverTooltip;
const tooltipEnterDelay = typeof enterDelay === 'number' ? enterDelay : theme.utils.tooltipEnterDelay;
const tooltipLeaveDelay = typeof leaveDelay === 'number' ? leaveDelay : theme.utils.tooltipLeaveDelay;
const titleText = props.title || intl.formatMessage({ id: titleId }, titleValues);
const slug = Utils.slugify(titleId);
return (
<span className={classNames('tooltip-intl', classes.root, className)}>
<Tooltip id={`tooltip-${slug}`}
title={titleText}
placement={placement}
enterDelay={tooltipEnterDelay}
leaveDelay={tooltipLeaveDelay}
classes={{
tooltip: classNames(classes.tooltip, tooltipClass),
popper: popperClass,
}}
>
<span className={classes.buttonWrap}>
{
variant === 'fab' && !!icon
?
<Button className={classNames(classes.button, slug)}
variant="fab"
aria-label={title}
ref={buttonRef}
{...properties}
>
{iconWithClass}
</Button>
:
!!icon
?
<IconButton className={classNames(classes.button, slug)}
aria-label={title}
ref={buttonRef}
{...properties}
>
{iconWithClass}
</IconButton>
:
variant === 'button'
?
<Button className={classNames(classes.button, slug)}
aria-label={title}
ref={buttonRef}
{...properties}
>
{children}
</Button>
:
children
}
</span>
</Tooltip>
</span>
);
};
TooltipIntl.propTypes = {
title: PropTypes.node,
titleId: PropTypes.string,
titleValues: PropTypes.object,
placement: PropTypes.string,
icon: PropTypes.node,
className: PropTypes.string,
classes: PropTypes.object,
buttonRef: PropTypes.func,
variant: PropTypes.string,
theme: PropTypes.object,
enterDelay: PropTypes.number,
leaveDelay: PropTypes.number,
parent: PropTypes.oneOf(['default', 'popover']),
children: PropTypes.node,
};
TooltipIntl.defaultProps = {
placement: 'bottom',
parent: 'default',
};
TooltipIntl.contextTypes = {
intl: intlShape.isRequired,
};
TooltipIntl.displayName = 'TooltipIntl';
registerComponent('TooltipIntl', TooltipIntl, [withStyles, styles], [withTheme]);

View file

@ -0,0 +1,212 @@
import React from 'react';
import PropTypes from 'prop-types';
import { intlShape } from 'meteor/vulcan:i18n';
import { replaceComponent, Components } from 'meteor/vulcan:core';
import moment from 'moment';
import withStyles from '@material-ui/core/styles/withStyles';
import IconButton from '@material-ui/core/IconButton';
import Checkbox from '@material-ui/core/Checkbox';
import EditIcon from 'mdi-material-ui/Pencil';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableRow from '@material-ui/core/TableRow';
import TableCell from '@material-ui/core/TableCell';
import classNames from 'classnames';
const getLabel = (field, fieldName, collection, intl) => {
const schema = collection.simpleSchema()._schema;
const fieldSchema = schema[fieldName];
if (fieldSchema) {
return intl.formatMessage(
{ id: `${collection._name}.${fieldName}`, defaultMessage: fieldSchema.label });
} else {
return fieldName;
}
};
const getTypeName = (field, fieldName, collection) => {
const schema = collection.simpleSchema()._schema;
const fieldSchema = schema[fieldName];
if (fieldSchema) {
const type = fieldSchema.type.singleType;
const typeName = typeof type === 'function' ? type.name : type;
return typeName;
} else {
return typeof field;
}
};
const parseImageUrl = value => {
const isImage = ['.png', '.jpg', '.gif'].indexOf(value.substr(-4)) !== -1 ||
['.webp', '.jpeg'].indexOf(value.substr(-5)) !== -1;
return isImage ?
<img style={{ width: '100%', maxWidth: 200 }} src={value} alt={value}/> :
<LimitedString string={value}/>;
};
const LimitedString = ({ string }) =>
<div>
{string.indexOf(' ') === -1 && string.length > 30 ?
<span title={string}>{string.substr(0, 30)}</span> :
<span>{string}</span>
}
</div>;
export const getFieldValue = (value, typeName, classes={}) => {
if (typeof value === 'undefined' || value === null) {
return '';
}
if (Array.isArray(value)) {
typeName = 'Array';
}
if (typeof typeName === 'undefined') {
typeName = typeof value;
}
switch (typeName) {
case 'Boolean':
case 'boolean':
return <Checkbox checked={value} disabled style={{ width: '32px', height: '32px' }}/>;
case 'Number':
case 'number':
case 'SimpleSchema.Integer':
return <code>{value.toString()}</code>;
case 'Array':
return <ol>{value.map(
(item, index) => <li key={index}>{getFieldValue(item, typeof item, classes)}</li>)}</ol>;
case 'Object':
case 'object':
return (
<Table className="table">
<TableBody>
{_.map(value, (value, key) =>
<TableRow className={classNames(classes.table, 'table')} key={key}>
<TableCell className={classNames(classes.tableHeadCell, 'datacard-label')} variant="head">{key}</TableCell>
<TableCell className={classNames(classes.tableCell, 'datacard-value')} >{getFieldValue(value, typeof value, classes)}</TableCell>
</TableRow>
)}
</TableBody>
</Table>
);
case 'Date':
return moment(new Date(value)).format('dddd, MMMM Do YYYY, h:mm:ss');
default:
return parseImageUrl(value);
}
};
const CardItem = ({ label, value, typeName, classes }) =>
<TableRow className={classes.tableRow}>
<TableCell className={classNames(classes.tableHeadCell, 'datacard-label')} variant="head">
{label}
</TableCell>
<TableCell className={classNames(classes.tableCell, 'datacard-value')}>
{getFieldValue(value, typeName, classes)}
</TableCell>
</TableRow>;
const CardEdit = (props, context) => {
const classes = props.classes;
const editTitle = context.intl.formatMessage({ id: 'cards.edit' });
return (
<TableRow className={classes.tableRow}>
<TableCell className={classes.tableCell} colSpan="2">
<Components.ModalTrigger label={editTitle}
component={<IconButton aria-label={editTitle}>
<EditIcon/>
</IconButton>}
>
<CardEditForm {...props} />
</Components.ModalTrigger>
</TableCell>
</TableRow>
);
};
CardEdit.contextTypes = { intl: intlShape };
const CardEditForm = ({ collection, document, closeModal }) =>
<Components.SmartForm
collection={collection}
documentId={document._id}
showRemove={true}
successCallback={document => {
closeModal();
}}
/>;
const styles = theme => ({
root: {},
table: {
maxWidth: '100%'
},
tableBody: {},
tableRow: {},
tableCell: {},
tableHeadCell: {},
});
const Card = ({ className, collection, document, currentUser, fields, classes }, { intl }) => {
const fieldNames = fields ? fields : _.without(_.keys(document), '__typename');
const canUpdate = currentUser && collection.options.mutations.update.check(currentUser, document);
return (
<div className={classNames(classes.root, 'datacard', `datacard-${collection._name}`, className)}>
<Table className={classNames(classes.table, 'table')} style={{ maxWidth: '100%' }}>
<TableBody>
{canUpdate ? <CardEdit collection={collection} document={document} classes={classes}/> : null}
{fieldNames.map((fieldName, index) =>
<CardItem key={index}
value={document[fieldName]}
typeName={getTypeName(document[fieldName], fieldName, collection)}
label={getLabel(document[fieldName], fieldName, collection, intl)}
classes={classes}
/>
)}
</TableBody>
</Table>
</div>
);
};
Card.displayName = 'Card';
Card.propTypes = {
className: PropTypes.string,
collection: PropTypes.object,
document: PropTypes.object,
currentUser: PropTypes.object,
fields: PropTypes.array,
classes: PropTypes.object.isRequired,
};
Card.contextTypes = {
intl: intlShape
};
replaceComponent('Card', Card, [withStyles, styles]);

View file

@ -0,0 +1,644 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import {
Components,
registerComponent,
replaceComponent,
withCurrentUser,
withMulti,
Utils
} from 'meteor/vulcan:core';
import { intlShape } from 'meteor/vulcan:i18n';
import withStyles from '@material-ui/core/styles/withStyles';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import TableCell from '@material-ui/core/TableCell';
import TableFooter from '@material-ui/core/TableFooter';
import Tooltip from '@material-ui/core/Tooltip';
import TableSortLabel from '@material-ui/core/TableSortLabel';
import TablePagination from '@material-ui/core/TablePagination';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import { getFieldValue } from './Card';
import _assign from 'lodash/assign';
import classNames from 'classnames';
/*
Datatable Component
*/
const baseStyles = theme => ({
root: {
position: 'relative',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
},
addButton: {
position: 'absolute',
top: '-8px',
right: 0,
},
search: {
marginBottom: theme.spacing.unit * 8,
},
table: {},
denseTable: {},
denserTable: {},
flatTable: {},
tableHead: {},
tableBody: {},
tableFooter: {},
tableRow: {},
tableHeadCell: {},
tableCell: {},
clickRow: {},
editCell: {},
editButton: {}
});
const delay = (function () {
var timer = 0;
return function (callback, ms) {
clearTimeout(timer);
timer = setTimeout(callback, ms);
};
})();
class Datatable extends PureComponent {
constructor (props) {
super(props);
this.updateQuery = this.updateQuery.bind(this);
this.state = {
value: '',
query: '',
currentSort: {},
};
}
toggleSort = column => {
let currentSort;
if (!this.state.currentSort[column]) {
currentSort = { [column]: 1 };
} else if (this.state.currentSort[column] === 1) {
currentSort = { [column]: -1 };
} else {
currentSort = {};
}
this.setState({ currentSort });
};
updateQuery (value) {
this.setState({
value: value
});
delay(() => {
this.setState({
query: value
});
}, 700);
}
render () {
if (this.props.data) {
return <Components.DatatableContents
columns={this.props.data.length ? Object.keys(this.props.data[0]) : undefined}
{...this.props}
results={this.props.data}
count={this.props.data.length}
totalCount={this.props.data.length}
showEdit={false}
showNew={false}
/>;
} else {
const {
className,
collection,
options,
showSearch,
showNew,
currentUser,
classes,
} = this.props;
const listOptions = {
collection: collection,
...options,
};
const DatatableWithMulti = withMulti(listOptions)(Components.DatatableContents);
// add _id to orderBy when we want to sort a column, to avoid breaking the graphql() hoc;
// see https://github.com/VulcanJS/Vulcan/issues/2090#issuecomment-433860782
// this.state.currentSort !== {} is always false, even when console.log(this.state.currentSort) displays
// {}. So we test on the length of keys for this object.
const orderBy = Object.keys(this.state.currentSort).length == 0 ? {} :
{ ...this.state.currentSort, _id: -1 };
return (
<div className={classNames('datatable', `datatable-${collection._name}`, classes.root,
className)}>
{/* DatatableAbove Component part*/}
{
showSearch &&
<Components.SearchInput value={this.state.query}
updateQuery={this.updateQuery}
className={classes.search}
/>
}
{
showNew &&
<Components.NewButton collection={collection}
variant="fab"
color="primary"
className={classes.addButton}
/>
}
<DatatableWithMulti {...this.props}
collection={collection}
terms={{ query: this.state.query, orderBy: orderBy }}
currentUser={this.props.currentUser}
toggleSort={this.toggleSort}
currentSort={this.state.currentSort}
/>
</div>
);
}
}
}
Datatable.propTypes = {
title: PropTypes.string,
className: PropTypes.string,
collection: PropTypes.object,
options: PropTypes.object,
columns: PropTypes.array,
showEdit: PropTypes.bool,
editComponent: PropTypes.func,
showNew: PropTypes.bool,
showSearch: PropTypes.bool,
emptyState: PropTypes.node,
currentUser: PropTypes.object,
classes: PropTypes.object,
data: PropTypes.array,
footerData: PropTypes.array,
dense: PropTypes.string,
queryDataRef: PropTypes.func,
rowClass: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
handleRowClick: PropTypes.func,
intlNamespace: PropTypes.string,
toggleSort: PropTypes.func,
currentSort: PropTypes.object,
paginate: PropTypes.bool,
};
Datatable.defaultProps = {
showNew: true,
showEdit: true,
showSearch: true,
paginate: false,
};
replaceComponent('Datatable', Datatable, withCurrentUser, [withStyles, baseStyles]);
/*
DatatableContents Component
*/
const datatableContentsStyles = theme => (_assign({}, baseStyles(theme), {
table: {
marginTop: theme.spacing.unit * 3,
marginBottom: theme.spacing.unit * 3,
},
denseTable: theme.utils.denseTable,
flatTable: theme.utils.flatTable,
denserTable: theme.utils.denserTable,
}));
const DatatableContents = ({
collection,
columns,
results,
loading,
loadMore,
count,
totalCount,
networkStatus,
refetch,
showEdit,
editComponent,
emptyState,
currentUser,
classes,
footerData,
dense,
queryDataRef,
rowClass,
handleRowClick,
intlNamespace,
title,
toggleSort,
currentSort,
paginate,
paginationTerms,
setPaginationTerms
}) => {
if (loading) {
return <Components.Loading/>;
} else if (!results || !results.length) {
return emptyState || null;
}
if (queryDataRef) queryDataRef(this.props);
const denseClass = dense && classes[dense + 'Table'];
// Pagination functions
const getPage = (paginationTerms) => (parseInt((paginationTerms.limit - 1) / paginationTerms.itemsPerPage));
const onChangePage = (event, page) => {
setPaginationTerms({
itemsPerPage: paginationTerms.itemsPerPage,
limit: (page + 1) * paginationTerms.itemsPerPage,
offset: page * paginationTerms.itemsPerPage
});
};
const onChangeRowsPerPage = (event) => {
let value = event.target.value;
let offset = Math.max(0, parseInt((paginationTerms.limit - paginationTerms.itemsPerPage) / value) * value);
let limit = Math.min(offset + value, totalCount);
setPaginationTerms({
itemsPerPage: value,
limit: limit,
offset: offset
});
};
return (
<React.Fragment>
{
title &&
<Toolbar>
<Typography variant="h6" id="tableTitle">
title
</Typography>
</Toolbar>
}
<Table className={classNames(classes.table, denseClass)}>
<TableHead className={classes.tableHead}>
<TableRow className={classes.tableRow}>
{
_.sortBy(columns, column => column.order).map(
(column, index) =>
<Components.DatatableHeader key={index}
collection={collection}
intlNamespace={intlNamespace}
column={column}
classes={classes}
toggleSort={toggleSort}
currentSort={currentSort}
/>
)
}
{
(showEdit || editComponent) &&
<TableCell className={classes.tableCell}/>
}
</TableRow>
</TableHead>
{
results &&
<TableBody className={classes.tableBody}>
{
results.map(
(document, index) =>
<Components.DatatableRow collection={collection}
columns={columns}
document={document}
refetch={refetch}
key={index}
showEdit={showEdit}
editComponent={editComponent}
currentUser={currentUser}
classes={classes}
rowClass={rowClass}
handleRowClick={handleRowClick}
/>)
}
</TableBody>
}
{
footerData &&
<TableFooter className={classes.tableFooter}>
<TableRow className={classes.tableRow}>
{
_.sortBy(columns, column => column.order).map(
(column, index) =>
<TableCell key={index} className={classNames(classes.tableCell, column.footerClass)}>
{footerData[index]}
</TableCell>
)
}
{
(showEdit || editComponent) &&
<TableCell className={classes.tableCell}/>
}
</TableRow>
</TableFooter>
}
</Table>
{
paginate &&
<TablePagination
component="div"
count={totalCount}
rowsPerPage={paginationTerms.itemsPerPage}
page={getPage(paginationTerms)}
backIconButtonProps={{
'aria-label': 'Previous Page',
}}
nextIconButtonProps={{
'aria-label': 'Next Page',
}}
onChangePage={onChangePage}
onChangeRowsPerPage={onChangeRowsPerPage}
/>
}
{
!paginate && loadMore &&
<Components.LoadMore className={classes.loadMore}
count={count}
totalCount={totalCount}
loadMore={loadMore}
networkStatus={networkStatus}
/>
}
</React.Fragment>
);
};
replaceComponent('DatatableContents', DatatableContents, [withStyles, datatableContentsStyles]);
/*
DatatableHeader Component
*/
const DatatableHeader = ({ collection, intlNamespace, column, classes, toggleSort, currentSort }, { intl }) => {
const columnName = typeof column === 'string' ? column : column.name || column.label;
let formattedLabel = '';
if (collection) {
const schema = collection.simpleSchema()._schema;
/*
use either:
1. the column name translation
2. the column name label in the schema (if the column name matches a schema field)
3. the raw column name.
*/
const defaultMessage = schema[columnName] ? schema[columnName].label : Utils.camelToSpaces(columnName);
formattedLabel = typeof columnName === 'string' ?
intl.formatMessage({
id: `${collection._name}.${columnName}`,
defaultMessage: defaultMessage
}) :
'';
// if sortable is a string, use it as the name of the property to sort by. If it's just `true`, use
// column.name
const sortPropertyName = typeof column.sortable === 'string' ? column.sortable : column.name;
if (column.sortable) {
return <Components.DatatableSorter name={sortPropertyName}
label={formattedLabel}
toggleSort={toggleSort}
currentSort={currentSort}
sortable={column.sortable}
/>;
}
} else if (intlNamespace) {
formattedLabel = typeof columnName === 'string' ?
intl.formatMessage({
id: `${intlNamespace}.${columnName}`,
defaultMessage: columnName
}) :
'';
} else {
formattedLabel = intl.formatMessage({ id: columnName, defaultMessage: columnName });
}
return <TableCell
className={classNames(classes.tableHeadCell, column.headerClass)}>{formattedLabel}</TableCell>;
};
DatatableHeader.contextTypes = {
intl: intlShape,
};
replaceComponent('DatatableHeader', DatatableHeader);
/*
DatatableSorter Component
*/
const DatatableSorter = ({ name, label, toggleSort, currentSort, sortable }) =>
<TableCell className="datatable-sorter"
sortDirection={!currentSort[name] ? false : currentSort[name] === 1 ? 'asc' : 'desc'}
>
<Tooltip
title="Sort"
placement='bottom-start'
enterDelay={300}
>
<TableSortLabel
active={!currentSort[name] ? false : true}
direction={currentSort[name] === 1 ? 'desc' : 'asc'}
onClick={() => toggleSort(name)}
>
{label}
</TableSortLabel>
</Tooltip>
</TableCell>;
replaceComponent('DatatableSorter', DatatableSorter);
/*
DatatableRow Component
*/
const datatableRowStyles = theme => (_assign({}, baseStyles(theme), {
clickRow: {
cursor: 'pointer',
},
editCell: {
paddingTop: '0 !important',
paddingBottom: '0 !important',
textAlign: 'right',
},
}));
const DatatableRow = ({
collection,
columns,
document,
refetch,
showEdit,
editComponent,
currentUser,
rowClass,
handleRowClick,
classes,
}, { intl }) => {
const EditComponent = editComponent;
if (typeof rowClass === 'function') {
rowClass = rowClass(document);
}
return (
<TableRow
className={classNames('datatable-item', classes.tableRow, rowClass, handleRowClick && classes.clickRow)}
onClick={handleRowClick && (event => handleRowClick(event, document))}
hover
>
{
_.sortBy(columns, column => column.order).map(
(column, index) =>
<Components.DatatableCell key={index}
column={column}
document={document}
currentUser={currentUser}
classes={classes}
/>)
}
{
(showEdit || editComponent) &&
<TableCell className={classes.editCell}>
{
EditComponent &&
<EditComponent collection={collection} document={document} refetch={refetch}/>
}
{
showEdit &&
<Components.EditButton collection={collection}
document={document}
buttonClasses={{ button: classes.editButton }}
/>
}
</TableCell>
}
</TableRow>
);
};
replaceComponent('DatatableRow', DatatableRow, [withStyles, datatableRowStyles]);
DatatableRow.contextTypes = {
intl: intlShape
};
/*
DatatableCell Component
*/
const DatatableCell = ({ column, document, currentUser, classes }) => {
const Component = column.component ||
Components[column.componentName] ||
Components.DatatableDefaultCell;
const columnName = typeof column === 'string' ? column : column.name;
const className = typeof columnName === 'string' ?
`datatable-item-${columnName.toLowerCase()}` :
'';
const cellClass = typeof column.cellClass === 'function' ?
column.cellClass({ column, document, currentUser }) :
typeof column.cellClass === 'string' ?
column.cellClass :
null;
return (
<TableCell className={classNames(classes.tableCell, cellClass, className)}>
<Component column={column}
document={document}
currentUser={currentUser}
/>
</TableCell>
);
};
replaceComponent('DatatableCell', DatatableCell);
/*
DatatableDefaultCell Component
*/
const DatatableDefaultCell = ({ column, document }) =>
<div>
{
typeof column === 'string'
?
getFieldValue(document[column])
:
getFieldValue(document[column.name])
}
</div>;
replaceComponent('DatatableDefaultCell', DatatableDefaultCell);

View file

@ -0,0 +1,86 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Components, registerComponent } from 'meteor/vulcan:core';
import { intlShape } from 'meteor/vulcan:i18n';
import EditIcon from 'mdi-material-ui/Pencil';
const EditButton = ({
collection,
document,
color = 'default',
variant,
triggerClasses,
buttonClasses,
...props
}, { intl }) => (
<Components.ModalTrigger
classes={triggerClasses}
component={<Components.TooltipIconButton titleId="datatable.edit"
icon={<EditIcon/>}
color={color}
variant={variant}
classes={buttonClasses}
/>}
>
<Components.EditForm collection={collection} document={document} {...props}/>
</Components.ModalTrigger>
);
EditButton.propTypes = {
collection: PropTypes.object.isRequired,
document: PropTypes.object.isRequired,
color: PropTypes.oneOf(['default', 'inherit', 'primary', 'secondary']),
variant: PropTypes.string,
triggerClasses: PropTypes.object,
buttonClasses: PropTypes.object,
};
EditButton.contextTypes = {
intl: intlShape
};
EditButton.displayName = 'EditButton';
registerComponent('EditButton', EditButton);
/*
EditForm Component
*/
const EditForm = ({ collection, document, closeModal, options, successCallback, removeSuccessCallback,...props }) => {
const success = successCallback
? () => {
successCallback();
closeModal();
}
: closeModal;
const remove = removeSuccessCallback
? () => {
removeSuccessCallback();
closeModal();
}
: closeModal;
return (
<Components.SmartForm
{...props}
collection={collection}
documentId={document && document._id}
showRemove={true}
successCallback={success}
removeSuccessCallback={remove}
/>
);
}
registerComponent('EditForm', EditForm);

View file

@ -0,0 +1,9 @@
import React from 'react';
import { replaceComponent } from 'meteor/vulcan:core';
import CircularProgress from '@material-ui/core/CircularProgress';
function Loading(props) {
return <CircularProgress {...props} />;
}
replaceComponent('Loading', Loading);

View file

@ -0,0 +1,150 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { intlShape } from 'meteor/vulcan:i18n';
import { registerComponent } from 'meteor/vulcan:core';
import withStyles from '@material-ui/core/styles/withStyles';
import Dialog from '@material-ui/core/Dialog';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import Button from '@material-ui/core/Button';
import classNames from 'classnames';
const styles = theme => ({
root: {
display: 'inline-block',
},
button: {},
anchor: {},
dialog: {},
dialogPaper: {},
dialogTitle: {},
dialogContent: {
paddingTop: '4px',
},
dialogOverflow: {
overflowY: 'visible',
},
});
class ModalTrigger extends PureComponent {
constructor (props) {
super(props);
this.state = { modalIsOpen: false };
}
componentDidMount() {
if (this.props.action) {
this.props.action({
openModal: this.openModal,
closeModal: this.closeModal,
});
}
}
openModal = () => {
this.setState({ modalIsOpen: true });
};
closeModal = () => {
this.setState({ modalIsOpen: false });
};
render () {
const {
className,
dialogClassName,
dialogOverflow,
labelId,
component,
titleId,
type,
children,
classes,
} = this.props;
const intl = this.context.intl;
const label = labelId ? intl.formatMessage({ id: labelId }) : this.props.label;
const title = titleId ? intl.formatMessage({ id: titleId }) : this.props.title;
const overflowClass = dialogOverflow && classes.dialogOverflow;
const triggerComponent = component
?
React.cloneElement(component, { onClick: this.openModal })
:
type === 'button'
?
<Button className={classes.button} variant="contained" onClick={this.openModal}>{label}</Button>
:
<a className={classes.anchor} href="#" onClick={this.openModal}>{label}</a>;
const childrenComponent = typeof children.type === 'function' ?
React.cloneElement(children, { closeModal: this.closeModal }) :
children;
return (
<span className={classNames('modal-trigger', classes.root, className)}>
{triggerComponent}
<Dialog className={classNames(dialogClassName)}
open={this.state.modalIsOpen}
onClose={this.closeModal}
fullWidth={true}
classes={{ paper: classNames(classes.dialogPaper, overflowClass) }}
>
{
title &&
<DialogTitle className={classes.dialogTitle}>{title}</DialogTitle>
}
<DialogContent className={classNames(classes.dialogContent, overflowClass)}>
{childrenComponent}
</DialogContent>
</Dialog>
</span>
);
}
}
ModalTrigger.propTypes = {
/**
* Callback fired when the component mounts.
* This is useful when you want to trigger an action programmatically.
* It supports `openModal()` and `closeModal()`.
*
* @param {object} actions This object contains all possible actions
* that can be triggered programmatically.
*/
action: PropTypes.func,
className: PropTypes.string,
dialogClassName: PropTypes.string,
dialogOverflow: PropTypes.bool,
label: PropTypes.string,
labelId: PropTypes.string,
component: PropTypes.object,
title: PropTypes.node,
titleId: PropTypes.string,
type: PropTypes.oneOf(['link', 'button']),
children: PropTypes.node,
classes: PropTypes.object,
};
ModalTrigger.contextTypes = {
intl: intlShape,
};
registerComponent('ModalTrigger', ModalTrigger, [withStyles, styles]);

View file

@ -0,0 +1,44 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Components, replaceComponent } from 'meteor/vulcan:core';
import { intlShape } from 'meteor/vulcan:i18n';
import AddIcon from 'mdi-material-ui/Plus';
const NewButton = ({
className,
collection,
color = 'default',
variant,
}, { intl }) => (
<Components.ModalTrigger
className={className}
component={<Components.TooltipIconButton titleId="datatable.new"
icon={<AddIcon/>}
color={color}
variant={variant}
/>}
>
<Components.EditForm collection={collection}/>
</Components.ModalTrigger>
);
NewButton.propTypes = {
className: PropTypes.string,
collection: PropTypes.object.isRequired,
color: PropTypes.oneOf(['default', 'inherit', 'primary', 'secondary']),
variant: PropTypes.string,
};
NewButton.contextTypes = {
intl: intlShape
};
NewButton.displayName = 'NewButton';
replaceComponent('NewButton', NewButton);

View file

@ -0,0 +1,129 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { intlShape } from 'meteor/vulcan:i18n';
import { Components, registerComponent, instantiateComponent } from 'meteor/vulcan:core';
import withStyles from '@material-ui/core/styles/withStyles';
import _omit from 'lodash/omit';
import classNames from 'classnames';
const styles = theme => ({
formInput: {
position: 'relative',
marginBottom: theme.spacing.unit * 3,
},
halfWidthLeft: {
display: 'inline-block',
width: '48%',
verticalAlign: 'top',
marginRight: '4%',
},
halfWidthRight: {
display: 'inline-block',
width: '48%',
verticalAlign: 'top',
},
thirdWidthLeft: {
display: 'inline-block',
width: '31%',
verticalAlign: 'top',
marginRight: '3.5%',
},
thirdWidthRight: {
display: 'inline-block',
width: '31%',
verticalAlign: 'top',
},
hidden: {
display: 'none',
},
});
class FormComponentInner extends PureComponent {
getProperties = () => {
return _omit(this.props, 'classes');
};
render () {
const {
classes,
inputClassName,
name,
input,
hidden,
beforeComponent,
afterComponent,
formInput,
intlInput,
nestedInput,
formComponents,
} = this.props;
const FormComponents = formComponents;
const inputClass = classNames(
classes.formInput,
hidden && classes.hidden,
inputClassName && classes[inputClassName],
`input-${name}`,
`form-component-${input || 'default'}`
);
const properties = this.getProperties();
const FormInput = formInput;
if (intlInput) {
return <Components.FormIntl {...properties} />;
} else if (nestedInput){
return <Components.FormNested {...properties} />;
} else {
return (
<div className={inputClass}>
{instantiateComponent(beforeComponent, properties)}
<FormInput {...properties}/>
{instantiateComponent(afterComponent, properties)}
</div>
);
}
}
}
FormComponentInner.contextTypes = {
intl: intlShape,
};
FormComponentInner.propTypes = {
classes: PropTypes.object.isRequired,
inputClassName: PropTypes.string,
name: PropTypes.string.isRequired,
input: PropTypes.any,
beforeComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
afterComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
errors: PropTypes.array.isRequired,
help: PropTypes.node,
onChange: PropTypes.func.isRequired,
showCharsRemaining: PropTypes.bool.isRequired,
charsRemaining: PropTypes.number,
charsCount: PropTypes.number,
max: PropTypes.number,
formInput: PropTypes.func.isRequired,
};
FormComponentInner.displayName = 'FormComponentInner';
registerComponent('FormComponentInner', FormComponentInner, [withStyles, styles]);

View file

@ -0,0 +1,51 @@
import React from 'react';
import PropTypes from 'prop-types';
import { replaceComponent, Components } from 'meteor/vulcan:core';
import { FormattedMessage } from 'meteor/vulcan:i18n';
import Snackbar from '@material-ui/core/Snackbar';
import withStyles from '@material-ui/core/styles/withStyles';
import classNames from 'classnames';
const styles = theme => ({
root: {
position: 'relative',
boxShadow: 'none',
marginBottom: theme.spacing.unit * 2
},
list: {
marginBottom: 0
},
error: { '& > div': { backgroundColor: theme.palette.error[500] } },
danger: { '& > div': { backgroundColor: theme.palette.error[500] } },
warning: { '& > div': { backgroundColor: theme.palette.error[500] } }
});
const FormErrors = ({ errors, classes }) => {
const messageNode = (
<ul className={classes.list}>
{errors.map((error, index) => (
<li key={index}>
<Components.FormError error={error} />
</li>
))}
</ul>
);
return (
<div>
{!!errors.length && (
<Snackbar
open={true}
className={classNames('flash-message', classes.root , classes.danger)}
message={messageNode}
/>
)}
</div>
);
};
replaceComponent('FormErrors', FormErrors, [withStyles, styles]);

View file

@ -0,0 +1,205 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { Components, replaceComponent, instantiateComponent, } from 'meteor/vulcan:core';
import Users from 'meteor/vulcan:users';
import withStyles from '@material-ui/core/styles/withStyles';
import Collapse from '@material-ui/core/Collapse';
import Typography from '@material-ui/core/Typography';
import Paper from '@material-ui/core/Paper';
import ExpandLessIcon from 'mdi-material-ui/ChevronUp';
import ExpandMoreIcon from 'mdi-material-ui/ChevronDown';
import classNames from 'classnames';
const styles = theme => ({
root: {
minWidth: '320px'
},
paper: {
padding: theme.spacing.unit * 3
},
subtitle1: {
display: 'flex',
alignItems: 'center',
paddingLeft: theme.spacing.unit / 2,
marginTop: theme.spacing.unit * 5,
marginBottom: theme.spacing.unit,
color: theme.palette.primary[500],
},
collapsible: {
cursor: 'pointer',
},
label: {},
toggle: {
'& svg': {
width: 21,
height: 21,
display: 'block',
}
},
container: {
paddingLeft: 4,
paddingRight: 4,
marginLeft: -4,
marginRight: -4,
},
entered: {
overflow: 'visible',
},
});
class FormGroup extends PureComponent {
constructor (props) {
super(props);
this.isAdmin = props.name === 'admin';
this.state = {
collapsed: props.startCollapsed || this.isAdmin,
};
}
toggle = () => {
const collapsible = this.props.collapsible || this.isAdmin;
if (!collapsible) return;
this.setState({
collapsed: !this.state.collapsed
});
};
renderHeading = () => {
const { classes, label } = this.props;
const collapsible = this.props.collapsible || this.isAdmin;
return (
<Typography className={classNames(classes.subtitle1, collapsible && classes.collapsible)}
variant="subtitle1"
onClick={this.toggle}
>
<div className={classes.label}>
{label}
</div>
{
collapsible &&
<div className={classes.toggle}>
{
this.state.collapsed
?
<ExpandMoreIcon/>
:
<ExpandLessIcon/>
}
</div>
}
</Typography>
);
};
// if at least one of the fields in the group has an error, the group as a whole has an error
hasErrors = () => _.some(this.props.fields, field => {
return !!this.props.errors.filter(error => error.path === field.path).length;
});
render () {
const {
name,
hidden,
classes,
currentUser,
} = this.props;
if (this.isAdmin && !Users.isAdmin(currentUser)) {
return null;
}
if (typeof hidden === 'function' ? hidden({ ...this.props }) : hidden) {
return null;
}
//do not display if no fields, no startComponent and no endComponent
if (!this.props.startComponent && !this.props.endComponent && !this.props.fields.length) {
return null;
}
const anchorName = name.split('.').length > 1 ? name.split('.')[1] : name;
const collapseIn = !this.state.collapsed || this.hasErrors();
const FormComponents = this.props.formComponents;
return (
<div className={classes.root}>
<a name={anchorName}/>
{
name === 'default'
?
null
:
this.renderHeading()
}
<Collapse classes={{ container: classes.container, entered: classes.entered }} in={collapseIn}>
<Paper className={classes.paper}>
{instantiateComponent(this.props.startComponent)}
{this.props.fields.map(field => (
<FormComponents.FormComponent
key={field.name}
disabled={this.props.disabled}
{...field}
errors={this.props.errors}
throwError={this.props.throwError}
currentValues={this.props.currentValues}
updateCurrentValues={this.props.updateCurrentValues}
deletedValues={this.props.deletedValues}
addToDeletedValues={this.props.addToDeletedValues}
clearFieldErrors={this.props.clearFieldErrors}
formType={this.props.formType}
currentUser={this.props.currentUser}
formComponents={FormComponents}
/>
))}
{instantiateComponent(this.props.endComponent)}
</Paper>
</Collapse>
</div>
);
}
}
FormGroup.propTypes = {
name: PropTypes.string,
label: PropTypes.string,
order: PropTypes.number,
hidden: PropTypes.bool,
fields: PropTypes.array,
collapsible: PropTypes.bool,
startCollapsed: PropTypes.bool,
updateCurrentValues: PropTypes.func,
startComponent: PropTypes.node,
endComponent: PropTypes.node,
currentUser: PropTypes.object,
};
replaceComponent('FormGroup', FormGroup, [withStyles, styles]);

View file

@ -0,0 +1,95 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import {
Components,
registerComponent,
instantiateComponent,
} from 'meteor/vulcan:core';
import withStyles from '@material-ui/core/styles/withStyles';
import Users from 'meteor/vulcan:users';
import classNames from 'classnames';
const styles = theme => ({
root: {
minWidth: '320px'
},
});
class FormGroupNone extends PureComponent {
render () {
const {
name,
hidden,
classes,
currentUser,
} = this.props;
if (this.isAdmin && !Users.isAdmin(currentUser)) {
return null;
}
if (typeof hidden === 'function' ? hidden({ ...this.props }) : hidden) {
return null;
}
//do not display if no fields, no startComponent and no endComponent
if (!this.props.startComponent && !this.props.endComponent && !this.props.fields.length) {
return null;
}
const anchorName = name.split('.').length > 1 ? name.split('.')[1] : name;
const FormComponents = this.props.formComponents;
return (
<div className={classes.root}>
<a name={anchorName}/>
{instantiateComponent(this.props.startComponent)}
{this.props.fields.map(field => (
<FormComponents.FormComponent
key={field.name}
disabled={this.props.disabled}
{...field}
errors={this.props.errors}
throwError={this.props.throwError}
currentValues={this.props.currentValues}
updateCurrentValues={this.props.updateCurrentValues}
deletedValues={this.props.deletedValues}
addToDeletedValues={this.props.addToDeletedValues}
clearFieldErrors={this.props.clearFieldErrors}
formType={this.props.formType}
currentUser={this.props.currentUser}
formComponents={FormComponents}
/>
))}
{instantiateComponent(this.props.endComponent)}
</div>
);
}
}
FormGroupNone.propTypes = {
name: PropTypes.string,
order: PropTypes.number,
hidden: PropTypes.bool,
fields: PropTypes.array,
updateCurrentValues: PropTypes.func,
startComponent: PropTypes.node,
endComponent: PropTypes.node,
currentUser: PropTypes.object,
};
registerComponent('FormGroupNone', FormGroupNone, [withStyles, styles]);

View file

@ -0,0 +1,198 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { Components, registerComponent, instantiateComponent } from 'meteor/vulcan:core';
import Users from 'meteor/vulcan:users';
import withStyles from '@material-ui/core/styles/withStyles';
import Collapse from '@material-ui/core/Collapse';
import Divider from '@material-ui/core/Divider';
import Typography from '@material-ui/core/Typography';
import ExpandLessIcon from 'mdi-material-ui/ChevronUp';
import ExpandMoreIcon from 'mdi-material-ui/ChevronDown';
import classNames from 'classnames';
const styles = theme => ({
root: {
minWidth: '320px',
},
divider: {
marginLeft: theme.spacing.unit * -3,
marginRight: theme.spacing.unit * -3,
},
subtitle1: {
marginTop: theme.spacing.unit * 5,
position: 'relative',
},
collapsible: {
cursor: 'pointer',
},
typography: {
display: 'flex',
alignItems: 'center',
'& > div': {
display: 'flex',
alignItems: 'center',
},
'& > div:first-child': {
...theme.typography.subtitle1,
},
paddingTop: theme.spacing.unit,
paddingBottom: theme.spacing.unit,
},
toggle: {
color: theme.palette.action.active,
},
entered: {
overflow: 'visible',
},
});
class FormGroupWithLine extends PureComponent {
constructor (props) {
super(props);
this.isAdmin = props.name === 'admin';
this.state = {
collapsed: props.startCollapsed || this.isAdmin,
};
}
toggle = () => {
const collapsible = this.props.collapsible || this.isAdmin;
if (!collapsible) return;
this.setState({
collapsed: !this.state.collapsed
});
};
renderHeading = () => {
const { classes } = this.props;
const collapsible = this.props.collapsible || this.isAdmin;
return (
<div className={classNames(classes.subtitle1, collapsible && classes.collapsible)} onClick={this.toggle}>
<Divider className={classes.divider}/>
<Typography className={classes.typography} variant="subtitle1" gutterBottom>
<div>
{this.props.label}
</div>
{
collapsible &&
<div className={classes.toggle}>
{
this.state.collapsed
?
<ExpandMoreIcon/>
:
<ExpandLessIcon/>
}
</div>
}
</Typography>
</div>
);
};
// if at least one of the fields in the group has an error, the group as a whole has an error
hasErrors = () => _.some(this.props.fields, field => {
return !!this.props.errors.filter(error => error.path === field.path).length;
});
render () {
const {
name,
hidden,
classes,
currentUser,
} = this.props;
if (this.isAdmin && !Users.isAdmin(currentUser)) {
return null;
}
if (typeof hidden === 'function' ? hidden({ ...this.props }) : hidden) {
return null;
}
//do not display if no fields, no startComponent and no endComponent
if (!this.props.startComponent && !this.props.endComponent && !this.props.fields.length) {
return null;
}
const anchorName = name.split('.').length > 1 ? name.split('.')[1] : name;
const collapseIn = !this.state.collapsed || this.hasErrors();
const FormComponents = this.props.formComponents;
return (
<div className={classes.root}>
<a name={anchorName}/>
{
this.props.name === 'default'
?
null
:
this.renderHeading()
}
<Collapse classes={{ entered: classes.entered }} in={collapseIn}>
{instantiateComponent(this.props.startComponent)}
{this.props.fields.map(field => (
<FormComponents.FormComponent
key={field.name}
disabled={this.props.disabled}
{...field}
errors={this.props.errors}
throwError={this.props.throwError}
currentValues={this.props.currentValues}
updateCurrentValues={this.props.updateCurrentValues}
deletedValues={this.props.deletedValues}
addToDeletedValues={this.props.addToDeletedValues}
clearFieldErrors={this.props.clearFieldErrors}
formType={this.props.formType}
currentUser={this.props.currentUser}
formComponents={FormComponents}
/>
))}
{instantiateComponent(this.props.endComponent)}
</Collapse>
</div>
);
}
}
FormGroupWithLine.propTypes = {
name: PropTypes.string,
label: PropTypes.string,
order: PropTypes.number,
fields: PropTypes.array,
collapsible: PropTypes.bool,
startCollapsed: PropTypes.bool,
updateCurrentValues: PropTypes.func,
startComponent: PropTypes.node,
endComponent: PropTypes.node,
currentUser: PropTypes.object,
};
registerComponent('FormGroupWithLine', FormGroupWithLine, [withStyles, styles]);

View file

@ -0,0 +1,13 @@
import React from 'react';
import { replaceComponent } from 'meteor/vulcan:core';
import Delete from 'mdi-material-ui/Delete';
import Plus from 'mdi-material-ui/Plus';
const IconRemove = () => <Delete/>;
replaceComponent('IconRemove', IconRemove);
const IconAdd = () => <Plus/>;
replaceComponent('IconAdd', IconAdd);

View file

@ -0,0 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
import { registerComponent } from 'meteor/vulcan:core';
import { FormattedMessage } from 'meteor/vulcan:i18n';
import withStyles from '@material-ui/core/styles/withStyles';
import Divider from '@material-ui/core/Divider';
const styles = theme => ({
divider: {
marginLeft: -24,
marginRight: -24,
marginTop: 16,
marginBottom: 23,
},
});
const FormNestedDivider = ({ classes, label, addItem }) => <Divider className={classes.divider}/>;
FormNestedDivider.propTypes = {
classes: PropTypes.object.isRequired,
label: PropTypes.string,
addItem: PropTypes.func,
};
registerComponent('FormNestedDivider', FormNestedDivider, [withStyles, styles]);

View file

@ -0,0 +1,20 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Components, registerComponent } from 'meteor/vulcan:core';
import { FormattedMessage } from 'meteor/vulcan:i18n';
import Grid from '@material-ui/core/Grid';
const FormNestedFoot = ({ label, addItem }) => (
<Grid container spacin={0} justify="flex-end">
<Components.Button color="primary" variant="fab" mini onClick={addItem} className="form-nested-add">
<Components.IconAdd/>
</Components.Button>
</Grid>
);
FormNestedFoot.propTypes = {
label: PropTypes.string,
addItem: PropTypes.func,
};
registerComponent('FormNestedFoot', FormNestedFoot);

View file

@ -0,0 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import { replaceComponent } from 'meteor/vulcan:core';
import { FormattedMessage } from 'meteor/vulcan:i18n';
const FormNestedHead = ({ label, addItem }) => <span/>;
FormNestedHead.propTypes = {
label: PropTypes.string,
addItem: PropTypes.func,
};
replaceComponent('FormNestedHead', FormNestedHead);

View file

@ -0,0 +1,136 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Components, replaceComponent } from 'meteor/vulcan:core';
import { intlShape } from 'meteor/vulcan:i18n';
import withStyles from '@material-ui/core/styles/withStyles';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import DeleteIcon from 'mdi-material-ui/Delete';
import Tooltip from '@material-ui/core/Tooltip';
import { FormattedMessage } from 'meteor/vulcan:i18n';
import classNames from 'classnames';
const styles = theme => ({
root: {
textAlign: 'center',
marginTop: theme.spacing.unit * 4,
},
button: {
margin: theme.spacing.unit,
},
delete: {
float: 'left',
},
tooltip: {
margin: 3,
}
});
const FormSubmit = ({
submitLabel,
cancelLabel,
cancelCallback,
revertLabel,
revertCallback,
document,
deleteDocument,
collectionName,
classes
}, {
intl,
isChanged,
clearForm,
}) => {
if (typeof isChanged !== 'function') {
isChanged = () => true;
}
return (
<div className={classes.root}>
{
deleteDocument
?
<Tooltip id={`tooltip-delete-${collectionName}`}
classes={{ tooltip: classNames('delete-button', classes.tooltip) }}
title={intl.formatMessage({ id: 'forms.delete' })}
placement="bottom">
<IconButton onClick={deleteDocument} className={classes.delete}>
<DeleteIcon/>
</IconButton>
</Tooltip>
:
null
}
{
cancelCallback
?
<Button variant="contained"
className={classNames('cancel-button', classes.button)}
onClick={(event) => {
event.preventDefault();
cancelCallback(document);
}}>
{cancelLabel ? cancelLabel : <FormattedMessage id="forms.cancel"/>}
</Button>
:
null
}
{
revertCallback
?
<Button variant="contained"
className={classNames('revert-button', classes.button)}
disabled={!isChanged()}
onClick={(event) => {
event.preventDefault();
clearForm({ clearErrors: true, clearCurrentValues: true, clearDeletedValues: true });
revertCallback(document);
}}
>
{revertLabel ? revertLabel : <FormattedMessage id="forms.revert"/>}
</Button>
:
null
}
<Button variant="contained"
type="submit"
color="secondary"
className={classNames('submit-button', classes.button)}
disabled={!isChanged()}
>
{submitLabel ? submitLabel : <FormattedMessage id="forms.submit"/>}
</Button>
</div>
);
};
FormSubmit.propTypes = {
submitLabel: PropTypes.node,
cancelLabel: PropTypes.node,
revertLabel: PropTypes.node,
cancelCallback: PropTypes.func,
revertCallback: PropTypes.func,
document: PropTypes.object,
deleteDocument: PropTypes.func,
collectionName: PropTypes.string,
classes: PropTypes.object,
};
FormSubmit.contextTypes = {
intl: intlShape,
isChanged: PropTypes.func,
clearForm: PropTypes.func,
};
replaceComponent('FormSubmit', FormSubmit, [withStyles, styles]);

View file

@ -0,0 +1,84 @@
import React from 'react';
import PropTypes from 'prop-types';
import { instantiateComponent } from 'meteor/vulcan:core';
import withStyles from '@material-ui/core/styles/withStyles';
import InputAdornment from '@material-ui/core/InputAdornment';
import IconButton from '@material-ui/core/IconButton';
import CloseIcon from 'mdi-material-ui/CloseCircle';
import classNames from 'classnames';
export const styles = theme => ({
inputAdornment: {
whiteSpace: 'nowrap',
marginTop: '0 !important',
'& > *': {
verticalAlign: 'bottom',
},
'& > svg': {
color: theme.palette.common.darkBlack,
},
'& > * + *': {
marginLeft: 8,
}
},
clearButton: {
opacity: 0,
'& svg': {
width: 20,
height: 20,
},
marginRight: -12,
marginLeft: -4,
'&:first-child': {
marginLeft: -12,
},
transition: theme.transitions.create('opacity', {
duration: theme.transitions.duration.short,
}),
},
urlButton: {
verticalAlign: 'bottom',
width: 24,
height: 24,
fontSize: 20,
}
});
const EndAdornment = (props) => {
const { classes, value, addonAfter, changeValue, hideClear, disabled } = props;
if (!addonAfter && (!changeValue || hideClear || disabled)) return null;
const hasValue = !!value || value === 0;
const clearButton = changeValue && !hideClear && !disabled &&
<IconButton className={classNames('clear-button', classes.clearButton, hasValue && 'clear-enabled')}
onClick={event => {
event.preventDefault();
changeValue(null);
}}
tabIndex="-1"
>
<CloseIcon/>
</IconButton>;
return (
<InputAdornment classes={{ root: classes.inputAdornment }} position="end">
{instantiateComponent(addonAfter)}
{clearButton}
</InputAdornment>
);
};
EndAdornment.propTypes = {
classes: PropTypes.object.isRequired,
value: PropTypes.any,
changeValue: PropTypes.func,
hideClear: PropTypes.bool,
addonAfter: PropTypes.oneOfType([PropTypes.string, PropTypes.node, PropTypes.func]),
};
export default withStyles(styles)(EndAdornment);

View file

@ -0,0 +1,150 @@
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import ComponentMixin from './mixins/component';
import withStyles from '@material-ui/core/styles/withStyles';
import FormGroup from '@material-ui/core/FormGroup';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import MuiFormControl from './MuiFormControl';
import MuiFormHelper from './MuiFormHelper';
import Checkbox from '@material-ui/core/Checkbox';
import Switch from '@material-ui/core/Switch';
import classNames from 'classnames';
const styles = theme => ({
group: {
marginTop: '8px',
},
twoColumn: {
display: 'block',
[theme.breakpoints.down('md')]: {
'& > label': {
marginRight: theme.spacing.unit * 5,
},
},
[theme.breakpoints.up('md')]: {
'& > label': {
width: '49%',
},
},
},
threeColumn: {
display: 'block',
[theme.breakpoints.down('xs')]: {
'& > label': {
marginRight: theme.spacing.unit * 5,
},
},
[theme.breakpoints.up('xs')]: {
'& > label': {
width: '49%',
},
},
[theme.breakpoints.up('md')]: {
'& > label': {
width: '32%',
},
},
},
});
const MuiCheckboxGroup = createReactClass({
mixins: [ComponentMixin],
propTypes: {
name: PropTypes.string.isRequired,
options: PropTypes.array.isRequired,
classes: PropTypes.object.isRequired,
variant: PropTypes.oneOf(['checkbox', 'switch']),
},
componentDidMount: function () {
if (this.props.refFunction) {
this.props.refFunction(this);
}
},
getDefaultProps: function () {
return {
label: '',
help: null,
variant: 'checkbox',
};
},
changeCheckbox: function () {
const value = [];
this.props.options.forEach(function (option, key) {
if (this[this.props.name + '-' + option.value].checked) {
value.push(option.value);
}
}.bind(this));
//this.setValue(value);
this.props.onChange(this.props.name, value);
},
validate: function () {
if (this.props.onBlur) {
this.props.onBlur();
}
return true;
},
renderElement: function () {
const controls = this.props.options.map((checkbox, key) => {
let value = checkbox.value;
let checked = (this.props.value.indexOf(value) !== -1);
let disabled = checkbox.disabled || this.props.disabled;
const Component = this.props.variant === 'switch' ? Switch : Checkbox;
return (
<FormControlLabel
key={key}
control={
<Component
inputRef={(c) => this[this.props.name + '-' + value] = c}
checked={checked}
onChange={this.changeCheckbox}
value={value}
disabled={disabled}
/>
}
label={checkbox.label}
/>
);
});
const maxLength = this.props.options.reduce((max, option) =>
option.label.length > max ? option.label.length : max, 0);
const columnClass = maxLength < 20 ? 'threeColumn' : maxLength < 30 ? 'twoColumn' : '';
return (
<FormGroup className={classNames(this.props.classes.group, this.props.classes[columnClass])}>
{controls}
</FormGroup>
);
},
render: function () {
if (this.props.layout === 'elementOnly') {
return (
<div>{this.renderElement()}</div>
);
}
return (
<MuiFormControl{...this.getFormControlProperties()} fakeLabel={true}>
{this.renderElement()}
<MuiFormHelper {...this.getFormHelperProperties()}/>
</MuiFormControl>
);
}
});
export default withStyles(styles)(MuiCheckboxGroup);

View file

@ -0,0 +1,91 @@
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import InputLabel from '@material-ui/core/InputLabel';
import FormControl from '@material-ui/core/FormControl';
import FormLabel from '@material-ui/core/FormLabel';
//noinspection JSUnusedGlobalSymbols
const MuiFormControl = createReactClass({
propTypes: {
label: PropTypes.node,
children: PropTypes.node,
required: PropTypes.bool,
hasErrors: PropTypes.bool,
fakeLabel: PropTypes.bool,
hideLabel: PropTypes.bool,
layout: PropTypes.oneOf(['horizontal', 'vertical', 'elementOnly']),
htmlFor: PropTypes.string
},
getDefaultProps: function () {
return {
label: '',
required: false,
hasErrors: false,
fakeLabel: false,
hideLabel: false,
};
},
renderRequiredSymbol: function () {
if (this.props.required === false) {
return null;
}
return (
<span className="required-symbol"> *</span>
);
},
renderLabel: function () {
if (this.props.layout === 'elementOnly' || this.props.hideLabel) {
return null;
}
if (this.props.fakeLabel) {
return (
<FormLabel className="control-label legend"
component="legend"
data-required={this.props.required}
>
{this.props.label}
{this.renderRequiredSymbol()}
</FormLabel>
);
}
const shrink = ['date', 'time', 'datetime'].includes(this.props.inputType) ? true : undefined;
return (
<InputLabel className="control-label"
data-required={this.props.required}
htmlFor={this.props.htmlFor}
shrink={shrink}
>
{this.props.label}
{this.renderRequiredSymbol()}
</InputLabel>
);
},
render: function () {
const { layout, className, children, hasErrors } = this.props;
if (layout === 'elementOnly') {
return <span>{children}</span>;
}
return (
<FormControl component="fieldset" error={hasErrors} fullWidth={true} className={className}>
{this.renderLabel()}
{children}
</FormControl>
);
}
});
export default MuiFormControl;

View file

@ -0,0 +1,75 @@
import React from 'react';
import PropTypes from 'prop-types';
import { instantiateComponent, Components } from 'meteor/vulcan:core';
import withStyles from '@material-ui/core/styles/withStyles';
import FormHelperText from '@material-ui/core/FormHelperText';
import { FormattedMessage } from 'meteor/vulcan:i18n';
import classNames from 'classnames';
export const styles = theme => ({
error: {
color: theme.palette.error.main,
},
formHelperText: {
display: 'flex',
'& :first-child': {
flexGrow: 1,
}
},
});
const MuiFormHelper = (props) => {
const {
classes,
help,
errors,
hasErrors,
showCharsRemaining,
charsRemaining,
charsCount,
max,
} = props;
if (!help && !hasErrors && !showCharsRemaining) {
return null;
}
const errorMessage = hasErrors &&
<Components.FormError error={errors[0]} />;
return (
<FormHelperText className={classes.formHelperText} error={hasErrors}>
<span>
{
hasErrors ? errorMessage : help
}
</span>
{
showCharsRemaining &&
<span className={charsRemaining < 0 ? classes.error : null}>
{charsCount} / {max}
</span>
}
</FormHelperText>
);
};
MuiFormHelper.propTypes = {
classes: PropTypes.object.isRequired,
value: PropTypes.any,
changeValue: PropTypes.func,
addonAfter: PropTypes.oneOfType([PropTypes.string, PropTypes.node, PropTypes.func]),
};
export default withStyles(styles)(MuiFormHelper);

View file

@ -0,0 +1,138 @@
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import withStyles from '@material-ui/core/styles/withStyles';
import ComponentMixin from './mixins/component';
import MuiFormControl from './MuiFormControl';
import MuiFormHelper from './MuiFormHelper';
import Input from '@material-ui/core/Input';
import StartAdornment, { hideStartAdornment, fixUrl } from './StartAdornment';
import EndAdornment from './EndAdornment';
export const styles = theme => ({
inputRoot: {
'& .clear-enabled': { opacity: 0 },
'&:hover .clear-enabled': { opacity: 0.54 },
},
inputFocused: {
'& .clear-enabled': { opacity: 0.54 }
},
});
//noinspection JSUnusedGlobalSymbols
const MuiInput = createReactClass({
element: null,
mixins: [ComponentMixin],
displayName: 'MuiInput',
propTypes: {
type: PropTypes.oneOf([
'color',
'date',
'datetime',
'datetime-local',
'email',
'hidden',
'month',
'number',
'password',
'range',
'search',
'tel',
'text',
'time',
'url',
'week'
]),
errors: PropTypes.array,
placeholder: PropTypes.string,
formatValue: PropTypes.func,
scrubValue: PropTypes.func,
hideClear: PropTypes.bool,
},
getDefaultProps: function () {
return {
type: 'text',
};
},
handleChange: function (event) {
let value = event.target.value;
if (this.props.scrubValue) {
value = this.props.scrubValue(value);
}
this.changeValue(value);
},
changeValue: function (value) {
this.props.onChange(this.props.name, value);
},
handleBlur: function (event) {
const { type, value } = this.props;
if (type === 'url' && !!value && value !== fixUrl(value)) {
this.changeValue(fixUrl(value))
}
},
render: function () {
const startAdornment = hideStartAdornment(this.props) ? null :
<StartAdornment {...this.props}
classes={null}
changeValue={this.changeValue}
/>;
const endAdornment =
<EndAdornment {...this.props}
classes={null}
changeValue={this.changeValue}
/>;
let element = this.renderElement(startAdornment, endAdornment);
if (this.props.layout === 'elementOnly' || this.props.type === 'hidden') {
return element;
}
return (
<MuiFormControl {...this.getFormControlProperties()} htmlFor={this.getId()}>
{element}
<MuiFormHelper {...this.getFormHelperProperties()}/>
</MuiFormControl>
);
},
renderElement: function (startAdornment, endAdornment) {
const { classes, disabled, autoFocus, formatValue } = this.props;
const value = formatValue ? formatValue(this.props.value) : this.props.value;
const options = this.props.options || {};
return (
<Input
ref={c => (this.element = c)}
{...this.cleanProps(this.props)}
id={this.getId()}
value={value}
onChange={this.handleChange}
onBlur={this.handleBlur}
disabled={disabled}
rows={options.rows || this.props.rows}
autoFocus={options.autoFocus || autoFocus}
startAdornment={startAdornment}
endAdornment={endAdornment}
placeholder={this.props.placeholder}
classes={{ root: classes.inputRoot, focused: classes.inputFocused }}
/>
);
},
});
export default withStyles(styles)(MuiInput);

View file

@ -0,0 +1,162 @@
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import ComponentMixin from './mixins/component';
import withStyles from '@material-ui/core/styles/withStyles';
import MuiFormControl from './MuiFormControl';
import MuiFormHelper from './MuiFormHelper';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Radio from '@material-ui/core/Radio';
import RadioGroup from '@material-ui/core/RadioGroup';
import classNames from 'classnames';
const styles = theme => ({
group: {
marginTop: '8px',
},
inline: {
flexDirection: 'row',
'& > label': {
marginRight: theme.spacing.unit * 5,
},
},
twoColumn: {
display: 'block',
[theme.breakpoints.down('md')]: {
'& > label': {
marginRight: theme.spacing.unit * 5,
},
},
[theme.breakpoints.up('md')]: {
'& > label': {
width: '49%',
},
},
},
threeColumn: {
display: 'block',
[theme.breakpoints.down('xs')]: {
'& > label': {
marginRight: theme.spacing.unit * 5,
},
},
[theme.breakpoints.up('xs')]: {
'& > label': {
width: '49%',
},
},
[theme.breakpoints.up('md')]: {
'& > label': {
width: '32%',
},
},
},
radio: {
width: '32px',
height: '32px',
marginLeft: '8px',
},
line: {
marginBottom: '12px',
},
});
const MuiRadioGroup = createReactClass({
mixins: [ComponentMixin],
propTypes: {
name: PropTypes.string.isRequired,
type: PropTypes.oneOf(['inline', 'stacked']),
options: PropTypes.array.isRequired
},
getInitialState: function () {
if (this.props.refFunction) {
this.props.refFunction(this);
}
},
getDefaultProps: function () {
return {
type: 'stacked',
label: '',
help: null,
classes: PropTypes.object.isRequired,
};
},
changeRadio: function (event) {
const value = event.target.value;
//this.setValue(value);
this.props.onChange(this.props.name, value);
},
validate: function () {
if (this.props.onBlur) {
this.props.onBlur();
}
return true;
},
renderElement: function () {
const controls = this.props.options.map((radio, key) => {
let checked = (this.props.value === radio.value);
let disabled = radio.disabled || this.props.disabled;
return (
<FormControlLabel
key={key}
value={radio.value}
control={<Radio
className={this.props.classes.radio}
inputRef={(c) => this['element-' + key] = c}
checked={checked}
disabled={disabled}
/>}
className={this.props.classes.line}
label={radio.label}
/>
);
});
const maxLength = this.props.options.reduce((max, option) =>
option.label.length > max ? option.label.length : max, 0);
let columnClass = maxLength < 18 ? 'threeColumn' : maxLength < 30 ? 'twoColumn' : '';
if (this.props.type === 'inline') columnClass = 'inline';
return (
<RadioGroup
aria-label={this.props.name}
name={this.props.name}
className={classNames(this.props.classes.group, this.props.classes[columnClass])}
value={this.props.value}
onChange={this.changeRadio}
>
{controls}
</RadioGroup>
);
},
render: function () {
if (this.props.layout === 'elementOnly') {
return (
<div>{this.renderElement()}</div>
);
}
return (
<MuiFormControl{...this.getFormControlProperties()} fakeLabel={true}>
{this.renderElement()}
<MuiFormHelper {...this.getFormHelperProperties()}/>
</MuiFormControl>
);
}
});
export default withStyles(styles)(MuiRadioGroup);

View file

@ -0,0 +1,210 @@
import withStyles from '@material-ui/core/styles/withStyles';
import React from 'react';
import createReactClass from 'create-react-class';
import ComponentMixin from './mixins/component';
import MuiFormControl from './MuiFormControl';
import MuiFormHelper from './MuiFormHelper';
import Select from '@material-ui/core/Select';
import Input from '@material-ui/core/Input';
import MenuItem from '@material-ui/core/MenuItem';
import MenuList from '@material-ui/core/MenuList';
import ListSubheader from '@material-ui/core/ListSubheader';
import StartAdornment, { hideStartAdornment } from './StartAdornment';
import EndAdornment from './EndAdornment';
import _isArray from 'lodash/isArray';
export const styles = theme => ({
inputRoot: {
'& .clear-enabled': { opacity: 0 },
'&:hover .clear-enabled': { opacity: 0.54 },
},
inputFocused: {
'& .clear-enabled': { opacity: 0.54 }
},
menuItem: {
paddingTop: 4,
paddingBottom: 4,
paddingLeft: 9,
fontFamily: theme.typography.fontFamily,
color: theme.palette.type === 'light' ? 'rgba(0, 0, 0, 0.87)' : theme.palette.common.white,
fontSize: theme.typography.pxToRem(16),
lineHeight: '1.1875em',
},
input: {
paddingLeft: 8,
},
});
const MuiSelect = createReactClass({
element: null,
mixins: [ComponentMixin],
getInitialState: function () {
return {
isOpen: false,
};
},
handleOpen: function () {
// this doesn't work
this.setState({
isOpen: true,
});
},
handleClose: function () {
// this doesn't work
this.setState({
isOpen: false,
});
},
handleChange: function (event) {
const target = event.target;
let value;
if (this.props.multiple) {
value = [];
for (let i = 0; i < target.length; i++) {
const option = target.options[i];
if (option.selected) {
value.push(option.value);
}
}
} else {
value = target.value;
}
this.changeValue(value);
},
changeValue: function (value) {
this.props.onChange(this.props.name, value);
},
render: function () {
if (this.props.layout === 'elementOnly') {
return this.renderElement();
}
return (
<MuiFormControl{...this.getFormControlProperties()} htmlFor={this.getId()}>
{this.renderElement()}
<MuiFormHelper {...this.getFormHelperProperties()}/>
</MuiFormControl>
);
},
renderElement: function () {
const renderOption = (item, key) => {
//eslint-disable-next-line no-unused-vars
const { group, label, ...rest } = item;
return this.props.native
?
<option key={key} {...rest}>{label}</option>
:
<MenuItem key={key} {...rest} className={classes.menuItem}>{label}</MenuItem>;
};
const renderGroup = (label, key, nodes) => {
return this.props.native
?
<optgroup label={label} key={key}>
{nodes}
</optgroup>
:
<MenuList subheader={<ListSubheader component="div">{label}</ListSubheader>} key={key}>
{nodes}
</MenuList>;
};
const { options, classes } = this.props;
let groups = options.filter(function (item) {
return item.group;
}).map(function (item) {
return item.group;
});
// Get the unique items in group.
groups = [...new Set(groups)];
let optionNodes = [];
if (groups.length === 0) {
optionNodes = options.map(function (item, index) {
return renderOption(item, index);
});
} else {
// For items without groups.
const itemsWithoutGroup = options.filter(function (item) {
return !item.group;
});
itemsWithoutGroup.forEach(function (item, index) {
optionNodes.push(renderOption(item, 'no-group-' + index));
});
groups.forEach(function (group, groupIndex) {
const groupItems = options.filter(function (item) {
return item.group === group;
});
const groupOptionNodes = groupItems.map(function (item, index) {
return renderOption(item, groupIndex + '-' + index);
});
optionNodes.push(renderGroup(group, groupIndex, groupOptionNodes));
});
}
let value = this.props.value;
if (!this.props.multiple && _isArray(value)) {
value = value.length ? value[0] : '';
}
const startAdornment = hideStartAdornment(this.props) ? null :
<StartAdornment {...this.props}
value={value}
classes={null}
/>;
const endAdornment =
<EndAdornment {...this.props}
value={value}
classes={null}
/>;
return (
<Select className="select"
ref={(c) => this.element = c}
{...this.cleanProps(this.props)}
value={value}
onChange={this.handleChange}
onOpen={this.handleOpen}
onClose={this.handleClose}
disabled={this.props.disabled}
input={<Input id={this.getId()}
startAdornment={startAdornment}
endAdornment={endAdornment}
classes={{
root: classes.inputRoot,
focused: classes.inputFocused,
input: classes.input,
}}
/>}
>
{optionNodes}
</Select>
);
}
});
export default withStyles(styles)(MuiSelect);

View file

@ -0,0 +1,437 @@
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import ComponentMixin from './mixins/component';
import withStyles from '@material-ui/core/styles/withStyles';
import Input from '@material-ui/core/Input';
import Autosuggest from 'react-autosuggest';
import Paper from '@material-ui/core/Paper';
import MenuItem from '@material-ui/core/MenuItem';
import match from 'autosuggest-highlight/match';
import parse from 'autosuggest-highlight/parse';
import { registerComponent } from 'meteor/vulcan:core';
import StartAdornment, { hideStartAdornment } from './StartAdornment';
import EndAdornment from './EndAdornment';
import MuiFormControl from './MuiFormControl';
import MuiFormHelper from './MuiFormHelper';
import _isEqual from 'lodash/isEqual';
import classNames from 'classnames';
import IsolatedScroll from 'react-isolated-scroll';
const maxSuggestions = 100;
/*{
container: 'react-autosuggest__container',
containerOpen: 'react-autosuggest__container--open',
input: 'react-autosuggest__input',
inputOpen: 'react-autosuggest__input--open',
inputFocused: 'react-autosuggest__input--focused',
suggestionsContainer: 'react-autosuggest__suggestions-container',
suggestionsContainerOpen: 'react-autosuggest__suggestions-container--open',
suggestionsList: 'react-autosuggest__suggestions-list',
suggestion: 'react-autosuggest__suggestion',
suggestionFirst: 'react-autosuggest__suggestion--first',
suggestionHighlighted: 'react-autosuggest__suggestion--highlighted',
sectionContainer: 'react-autosuggest__section-container',
sectionContainerFirst: 'react-autosuggest__section-container--first',
sectionTitle: 'react-autosuggest__section-title'
}*/
const styles = theme => ({
container: {
flexGrow: 1,
position: 'relative',
},
textField: {
width: '100%',
'label + div > &': {
marginTop: theme.spacing.unit * 2,
},
},
input: {
outline: 0,
font: 'inherit',
color: 'currentColor',
width: '100%',
border: '0',
margin: '0',
padding: '7px 0',
display: 'block',
boxSizing: 'content-box',
background: 'none',
verticalAlign: 'middle',
'&::-webkit-search-decoration, &::-webkit-search-cancel-button, &::after, &:after':
{ display: 'none' },
'&::-webkit-search-results, &::-webkit-search-results-decoration':
{ display: 'none' },
},
readOnly: {
cursor: 'pointer',
},
suggestionsContainer: {
display: 'none',
position: 'absolute',
left: 0,
right: 0,
zIndex: theme.zIndex.modal,
marginBottom: theme.spacing.unit * 3,
maxHeight: 48 * 8,
},
suggestionsContainerOpen: {
display: 'flex',
},
scroller: {
flexGrow: 1,
overflowY: 'auto',
},
suggestion: {
display: 'block',
},
suggestionIcon: {
marginRight: theme.spacing.unit * 2,
},
selected: {
backgroundColor: theme.palette.secondary.light,
},
suggestionsList: {
margin: 0,
padding: 0,
listStyleType: 'none',
},
inputRoot: {
'& .clear-enabled': { opacity: 0 },
'&:hover .clear-enabled': { opacity: 0.54 },
},
inputFocused: {
'& .clear-enabled': { opacity: 0.54 }
},
});
const MuiSuggest = createReactClass({
inputElement: null,
mixins: [ComponentMixin],
propTypes: {
options: PropTypes.arrayOf(PropTypes.shape({
label: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
formatted: PropTypes.node,
iconComponent: PropTypes.node,
onClick: PropTypes.func,
})),
classes: PropTypes.object.isRequired,
limitToList: PropTypes.bool,
disableText: PropTypes.bool,
showAllOptions: PropTypes.bool,
className: PropTypes.string,
autoComplete: PropTypes.string,
autoFocus: PropTypes.bool,
},
getDefaultProps: function () {
return {
autoComplete: 'off',
autoFocus: false,
};
},
getOptionFormatted: function (option) {
return option.formatted || option.label || option.value || '';
},
getOptionLabel: function (option) {
return option.label || option.value || '';
},
getInitialState: function () {
if (this.props.refFunction) {
this.props.refFunction(this);
}
const selectedOption = this.getSelectedOption();
return {
inputValue: this.getOptionLabel(selectedOption),
selectedOption: selectedOption,
suggestions: [],
};
},
componentWillReceiveProps: function (nextProps) {
if (nextProps.value !== this.state.value ||
nextProps.options !== this.props.options) {
const selectedOption = this.getSelectedOption(nextProps);
this.setState({
inputValue: this.getOptionLabel(selectedOption),
selectedOption: selectedOption,
});
}
},
shouldComponentUpdate: function (nextProps, nextState) {
return !_isEqual(nextState, this.state) ||
nextProps.help !== this.props.help ||
nextProps.charsCount !== this.props.charsCount ||
!_isEqual(nextProps.errors, this.props.errors) ||
nextProps.options !== this.props.options;
},
getSelectedOption: function (props) {
props = props || this.props;
const selectedOption = props.options.find((opt) => opt.value === props.value);
return selectedOption || { label: '', value: null };
},
handleFocus: function (event) {
if (!this.inputElement) return;
this.inputElement.select();
},
handleBlur: function (event, { highlightedSuggestion: suggestion }) {
if (suggestion) {
this.changeValue(suggestion);
} else if (this.props.limitToList) {
const selectedOption = this.getSelectedOption();
this.setState({
inputValue: this.getOptionLabel(selectedOption),
});
}
},
suggestionSelected: function (event, { suggestion }) {
this.changeValue(suggestion);
},
changeValue: function (suggestion) {
if (!suggestion) {
suggestion = { label: '', value: null };
}
if (suggestion.onClick) {
return;
}
this.setState({
selectedOption: suggestion,
inputValue: this.getOptionLabel(suggestion),
});
this.props.onChange(this.props.name, suggestion.value, this.getOptionLabel(suggestion));
},
handleInputChange: function (event) {
const value = event.target.value;
this.setState({
inputValue: value,
});
},
handleSuggestionsFetchRequested: function ({ value, reason }) {
this.setState({
suggestions: this.getSuggestions(value),
});
},
handleSuggestionsClearRequested: function () {
this.setState({
suggestions: [],
});
},
shouldRenderSuggestions: function (value) {
return true;
},
render: function () {
const value = this.props.value;
const startAdornment = hideStartAdornment(this.props) ? null :
<StartAdornment {...this.props}
value={value}
classes={null}
/>;
const endAdornment =
<EndAdornment {...this.props}
value={value}
classes={null}
changeValue={this.changeValue}
/>;
const element = this.renderElement(startAdornment, endAdornment);
if (this.props.layout === 'elementOnly') {
return element;
}
return (
<MuiFormControl{...this.getFormControlProperties()} htmlFor={this.getId()}>
{element}
<MuiFormHelper {...this.getFormHelperProperties()}/>
</MuiFormControl>
);
},
renderElement: function (startAdornment, endAdornment) {
const { classes, autoFocus, disableText, showAllOptions } = this.props;
return (
<Autosuggest
theme={{
container: classes.container,
input: classNames(classes.input, this.props.disableText && classes.readOnly),
suggestionsContainer: classes.suggestionsContainer,
suggestionsContainerOpen: classes.suggestionsContainerOpen,
suggestion: classes.suggestion,
suggestionsList: classes.suggestionsList,
}}
highlightFirstSuggestion={!disableText && !showAllOptions}
renderInputComponent={this.renderInputComponent}
suggestions={this.state.suggestions}
onSuggestionsFetchRequested={this.handleSuggestionsFetchRequested}
onSuggestionsClearRequested={this.handleSuggestionsClearRequested}
renderSuggestionsContainer={this.renderSuggestionsContainer}
shouldRenderSuggestions={this.shouldRenderSuggestions}
focusInputOnSuggestionClick={false}
alwaysRenderSuggestions={false}
getSuggestionValue={this.getSuggestionValue}
renderSuggestion={this.renderSuggestion}
onSuggestionSelected={this.suggestionSelected}
inputProps={{
autoFocus,
classes,
onChange: this.handleInputChange,
onFocus: this.handleFocus,
onBlur: this.handleBlur,
value: this.state.inputValue,
readOnly: this.props.disableText,
disabled: this.props.disabled,
name: this.props.name,
'aria-haspopup': 'true',
...this.props.inputProps,
startAdornment,
endAdornment,
}}
/>
);
},
renderInputComponent: function (inputProps) {
const { classes, autoFocus, autoComplete, value, ref, startAdornment, endAdornment, disabled, ...rest } = inputProps;
return (
<Input
autoFocus={autoFocus}
autoComplete={autoComplete}
className={classes.textField}
classes={{ root: classes.inputRoot, focused: classes.inputFocused }}
value={value}
inputRef={c => { ref(c); this.inputElement = c; }}
type="text"
startAdornment={startAdornment}
endAdornment={endAdornment}
disabled={disabled}
inputProps={{
...rest,
}}
/>
);
},
renderSuggestion: function (suggestion, { query, isHighlighted }) {
const label = this.getOptionFormatted(suggestion);
const matches = match(label, query);
const parts = parse(label, matches);
const isSelected = suggestion.value === this.props.value;
const className = isSelected ? this.props.classes.selected : null;
return (
<MenuItem selected={isHighlighted}
component="div"
className={className}
onClick={suggestion.onClick}
data-value={suggestion.value}
>
{
suggestion.iconComponent &&
<div className={this.props.classes.suggestionIcon}>
{suggestion.iconComponent}
</div>
}
<div>
{parts.map((part, index) => {
return part.highlight ? (
<span key={index} style={{ fontWeight: 500 }}>
{part.text}
</span>
) : (
<strong key={index} style={{ fontWeight: 300 }}>
{part.text}
</strong>
);
})}
</div>
</MenuItem>
);
},
renderSuggestionsContainer: function ({ containerProps, children }) {
const { classes } = this.props;
return (
<Paper {...containerProps} id={`menu-${this.props.name}`} square>
<IsolatedScroll className={classes.scroller}>
{children}
</IsolatedScroll>
</Paper>
);
},
getSuggestionValue: function (suggestion) {
return suggestion.value;
},
getSuggestions: function (value) {
const inputValue = value.trim().toLowerCase();
const inputLength = inputValue.length;
let count = 0;
const inputMatchesSelection = value === this.getOptionLabel(this.state.selectedOption);
return (this.props.disableText || this.props.showAllOptions) && inputMatchesSelection ?
this.props.options.filter(suggestion => {
return true;
})
:
inputLength === 0
?
this.props.options.filter(suggestion => {
count += 1;
return count <= maxSuggestions;
})
:
this.props.options.filter(suggestion => {
const label = this.getOptionLabel(suggestion);
const keep =
count < maxSuggestions && label.toLowerCase().slice(0, inputLength) ===
inputValue;
if (keep) {
count += 1;
}
return keep;
});
},
});
export default withStyles(styles)(MuiSuggest);
registerComponent('MuiSuggest', MuiSuggest, [withStyles, styles]);

View file

@ -0,0 +1,71 @@
import React from 'react';
import createReactClass from 'create-react-class';
import ComponentMixin from './mixins/component';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Switch from '@material-ui/core/Switch';
import MuiFormControl from './MuiFormControl';
import MuiFormHelper from './MuiFormHelper';
const MuiSwitch = createReactClass({
mixins: [ComponentMixin],
getDefaultProps: function () {
return {
label: '',
rowLabel: '',
value: false
};
},
changeValue: function (event) {
const target = event.target;
const value = target.checked;
//this.setValue(value);
this.props.onChange(this.props.name, value);
setTimeout(() => {document.activeElement.blur();});
},
render: function () {
const element = this.renderElement();
if (this.props.layout === 'elementOnly') {
return element;
}
return (
<MuiFormControl {...this.getFormControlProperties()} label={this.props.rowLabel}
htmlFor={this.getId()}
>
{element}
<MuiFormHelper {...this.getFormHelperProperties()}/>
</MuiFormControl>
);
},
renderElement: function () {
return (
<FormControlLabel
control={
<Switch
ref={(c) => this.element = c}
{...this.cleanSwitchProps(this.cleanProps(this.props))}
id={this.getId()}
checked={this.props.value === true}
onChange={this.changeValue}
disabled={this.props.disabled}
/>
}
label={this.props.label}
/>
);
},
});
export default MuiSwitch;

View file

@ -0,0 +1,54 @@
import PropTypes from 'prop-types';
import React from 'react';
import { instantiateComponent } from 'meteor/vulcan:core';
import withStyles from '@material-ui/core/styles/withStyles';
import InputAdornment from '@material-ui/core/InputAdornment';
import IconButton from '@material-ui/core/IconButton';
import OpenInNewIcon from 'mdi-material-ui/OpenInNew';
import { styles } from './EndAdornment';
export const hideStartAdornment = (props) => {
return !props.addonBefore && !props.isUrl;
};
export const fixUrl = (url) => {
return url.indexOf('http://') === -1 && url.indexOf('https://') ? 'http://' + url : url;
};
const StartAdornment = (props) => {
const { classes, value, type, addonBefore } = props;
if (hideStartAdornment(props)) return null;
const urlButton = type === 'url' &&
<IconButton
className={classes.urlButton}
href={fixUrl(value)}
target="_blank"
disabled={!value}
>
<OpenInNewIcon/>
</IconButton>;
return (
<InputAdornment classes={{ root: classes.inputAdornment }} position="start">
{instantiateComponent(addonBefore)}
{urlButton}
</InputAdornment>
);
};
StartAdornment.propTypes = {
classes: PropTypes.object.isRequired,
value: PropTypes.any,
type: PropTypes.string,
addonBefore: PropTypes.oneOfType([PropTypes.string, PropTypes.node, PropTypes.func]),
};
export default withStyles(styles)(StartAdornment);

View file

@ -0,0 +1,139 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'meteor/vulcan:i18n';
import _omit from 'lodash/omit';
export default {
propTypes: {
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
hideLabel: PropTypes.bool,
layout: PropTypes.string,
required: PropTypes.bool,
errors: PropTypes.arrayOf(PropTypes.object),
},
getFormControlProperties: function () {
return {
label: this.props.label,
hideLabel: this.props.hideLabel,
layout: this.props.layout,
required: this.props.required,
hasErrors: this.hasErrors(),
className: this.props.className,
inputType: this.props.inputType,
};
},
getFormHelperProperties: function () {
return {
help: this.props.help,
errors: this.props.errors,
hasErrors: this.hasErrors(),
showCharsRemaining: this.props.showCharsRemaining,
charsRemaining: this.props.charsRemaining,
charsCount: this.props.charsCount,
max: this.props.max,
};
},
hashString: function (string) {
let hash = 0;
for (let i = 0; i < string.length; i++) {
hash = (((hash << 5) - hash) + string.charCodeAt(i)) & 0xFFFFFFFF;
}
return hash;
},
/**
* The ID is used as an attribute on the form control, and is used to allow
* associating the label element with the form control.
*
* If we don't explicitly pass an `id` prop, we generate one based on the
* `name`, `label` and `itemIndex` (for nested forms) properties.
*/
getId: function () {
if (this.props.id) {
return this.props.id;
}
const label = (typeof this.props.label === 'undefined' ? '' : this.props.label);
const itemIndex = (typeof this.props.itemIndex=== 'undefined' ? '' : this.props.itemIndex);
return [
'frc',
this.props.name.split('[').join('_').replace(']', ''),
itemIndex,
this.hashString(JSON.stringify(label))
].join('-');
},
hasErrors: function () {
return !!(this.props.errors && this.props.errors.length);
},
cleanProps: function (props) {
const removedFields = [
'beforeComponent',
'afterComponent',
'addonAfter',
'addonBefore',
'help',
'label',
'hideLabel',
'options',
'layout',
'rowLabel',
'validatePristine',
'validateOnSubmit',
'inputClassName',
'optional',
'throwError',
'currentValues',
'addToDeletedValues',
'deletedValues',
'clearFieldErrors',
'formType',
'inputType',
'showCharsRemaining',
'charsCount',
'charsRemaining',
'handleChange',
'document',
'updateCurrentValues',
'classes',
'errors',
'description',
'clearField',
'regEx',
'mustComplete',
'renderComponent',
'formInput',
'className',
'formatValue',
'scrubValue',
'custom',
'hideClear',
'inputProperties',
'currentUser',
'nestedSchema',
'parentFieldName',
'itemIndex',
'formComponents',
'autoValue',
'minCount',
'maxCount'
];
return _omit(props, removedFields);
},
cleanSwitchProps: function (props) {
const removedFields = [
'value',
'error',
];
return _omit(props, removedFields);
},
};

View file

@ -0,0 +1,10 @@
import React from 'react';
import MuiSwitch from '../base-controls/MuiSwitch';
import { registerComponent } from 'meteor/vulcan:core';
const CheckboxComponent = ({ refFunction, ...properties }) =>
<MuiSwitch {...properties} ref={refFunction}/>;
registerComponent('FormComponentCheckbox', CheckboxComponent);

View file

@ -0,0 +1,10 @@
import React from 'react';
import MuiCheckboxGroup from '../base-controls/MuiCheckboxGroup';
import { registerComponent } from 'meteor/vulcan:core';
const CheckboxGroupComponent = ({ refFunction, ...properties }) =>
<MuiCheckboxGroup {...properties} ref={refFunction}/>;
registerComponent('FormComponentCheckboxGroup', CheckboxGroupComponent);

View file

@ -0,0 +1,11 @@
import React from 'react';
import MuiSuggest from '../base-controls/MuiSuggest';
import { registerComponent } from 'meteor/vulcan:core';
import { countries } from './countries';
const CountrySelect = ({ refFunction, ...properties }) =>
<MuiSuggest {...properties} ref={refFunction} options={countries} limitToList={true}/>;
registerComponent('CountrySelect', CountrySelect);

View file

@ -0,0 +1,37 @@
import React from 'react';
import MuiInput from '../base-controls/MuiInput';
import { registerComponent } from 'meteor/vulcan:core';
import withStyles from '@material-ui/core/styles/withStyles';
export const styles = theme => ({
'@global': {
'input[type=date]::-ms-clear, input[type=date]::-ms-reveal': {
display: 'none',
width: 0,
height: 0,
},
'input[type=date]::-webkit-search-cancel-button': {
display: 'none',
'-webkit-appearance': 'none',
},
'input[type="date"]::-webkit-clear-button': {
display: 'none',
'-webkit-appearance': 'none',
},
'input[type="date"]::-webkit-inner-spin-button,input[type="date"]::-webkit-outer-spin-button': {
'-webkit-appearance': 'none',
margin: 0,
},
},
});
const DateComponent = ({ refFunction, classes, ...properties }) =>
<MuiInput {...properties} ref={refFunction} type="date"/>;
registerComponent('FormComponentDate', DateComponent, [withStyles, styles]);

View file

@ -0,0 +1,60 @@
// Deprecated react-datetime version
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import DateTimePicker from 'react-datetime';
import { 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});
}
render() {
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>
);
}
}
DateComponent.propTypes = {
control: PropTypes.any,
datatype: PropTypes.any,
group: PropTypes.any,
label: PropTypes.string,
name: PropTypes.string,
value: PropTypes.any,
};
DateComponent.contextTypes = {
updateCurrentValues: PropTypes.func,
};
export default DateComponent;

View file

@ -0,0 +1,37 @@
import React from 'react';
import MuiInput from '../base-controls/MuiInput';
import { registerComponent } from 'meteor/vulcan:core';
import withStyles from '@material-ui/core/styles/withStyles';
export const styles = theme => ({
'@global': {
'input[type=datetime]::-ms-clear, input[type=datetime]::-ms-reveal': {
display: 'none',
width: 0,
height: 0,
},
'input[type=datetime]::-webkit-search-cancel-button': {
display: 'none',
'-webkit-appearance': 'none',
},
'input[type="datetime"]::-webkit-clear-button': {
display: 'none',
'-webkit-appearance': 'none',
},
'input[type="datetime"]::-webkit-inner-spin-button,input[type="datetime"]::-webkit-outer-spin-button': {
'-webkit-appearance': 'none',
margin: 0,
},
},
});
const DateTimeComponent = ({ refFunction, classes, ...properties }) =>
<MuiInput {...properties} ref={refFunction} type="datetime-local"/>;
registerComponent('FormComponentDateTime', DateTimeComponent, [withStyles, styles]);

View file

@ -0,0 +1,61 @@
// Deprecated react-datetime version
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import DateTimePicker from 'react-datetime';
import { registerComponent } from 'meteor/vulcan:core';
class DateTimeRdt 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.name]: date});
}
render() {
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>
);
}
}
DateTimeRdt.propTypes = {
control: PropTypes.any,
datatype: PropTypes.any,
group: PropTypes.any,
label: PropTypes.string,
name: PropTypes.string,
value: PropTypes.any,
};
DateTimeRdt.contextTypes = {
updateCurrentValues: PropTypes.func,
};
export default DateTimeRdt;

View file

@ -0,0 +1,10 @@
import React from 'react';
import MuiInput from '../base-controls/MuiInput';
import { registerComponent } from 'meteor/vulcan:core';
const Default = ({ refFunction, ...properties }) =>
<MuiInput {...properties} ref={refFunction}/>;
registerComponent('FormComponentDefault', Default);

View file

@ -0,0 +1,10 @@
import React from 'react';
import MuiInput from '../base-controls/MuiInput';
import { registerComponent } from 'meteor/vulcan:core';
const EmailComponent = ({ refFunction, ...properties }) =>
<MuiInput {...properties} ref={refFunction} type="email" />;
registerComponent('FormComponentEmail', EmailComponent);

View file

@ -0,0 +1,10 @@
import React from 'react';
import MuiInput from '../base-controls/MuiInput';
import { registerComponent } from 'meteor/vulcan:core';
const NumberComponent = ({ refFunction, ...properties }) =>
<MuiInput {...properties} ref={refFunction} type="number" />;
registerComponent('FormComponentNumber', NumberComponent);

View file

@ -0,0 +1,15 @@
import React from 'react';
import MuiInput from '../base-controls/MuiInput';
import { registerComponent } from 'meteor/vulcan:core';
import { getCountryInfo } from './RegionSelect';
const PostalCode = ({ classes, refFunction, ...properties }) => {
const currentCountryInfo = getCountryInfo(properties);
const postalLabel = currentCountryInfo ? currentCountryInfo.postalLabel : 'Postal code';
return <MuiInput {...properties} ref={refFunction} label={postalLabel}/>;
};
registerComponent('PostalCode', PostalCode);

View file

@ -0,0 +1,10 @@
import React from 'react';
import MuiRadioGroup from '../base-controls/MuiRadioGroup';
import { registerComponent } from 'meteor/vulcan:core';
const RadioGroupComponent = ({ refFunction, ...properties }) =>
<MuiRadioGroup {...properties} ref={refFunction}/>;
registerComponent('FormComponentRadioGroup', RadioGroupComponent);

View file

@ -0,0 +1,31 @@
import React from 'react';
import MuiSuggest from '../base-controls/MuiSuggest';
import MuiInput from '../base-controls/MuiInput';
import { registerComponent } from 'meteor/vulcan:core';
import { countryInfo } from './countries';
import _get from 'lodash/get';
export const getCountryInfo = function (formComponentProps) {
const addressPath = formComponentProps.path;
const countryParts = addressPath.split('.');
countryParts[countryParts.length-1] = 'country';
const country = _get(formComponentProps.document, countryParts);
return country && countryInfo[country];
};
const RegionSelect = ({ classes, refFunction, ...properties }) => {
const currentCountryInfo = getCountryInfo(properties);
const options = currentCountryInfo ? currentCountryInfo.regions : null;
const regionLabel = currentCountryInfo ? currentCountryInfo.regionLabel : 'Region';
if (options) {
return <MuiSuggest {...properties} ref={refFunction} options={options} label={regionLabel}/>;
} else {
return <MuiInput {...properties} ref={refFunction} label={regionLabel}/>;
}
};
registerComponent('RegionSelect', RegionSelect);

View file

@ -0,0 +1,14 @@
import React from 'react';
import MuiSelect from '../base-controls/MuiSelect';
import { registerComponent } from 'meteor/vulcan:core';
const SelectComponent = ({ refFunction, ...properties }) => {
const noneOption = { label: '', value: '' };
properties.options = [noneOption, ...properties.options];
return <MuiSelect {...properties} ref={refFunction}/>;
};
registerComponent('FormComponentSelect', SelectComponent);

View file

@ -0,0 +1,13 @@
import React from 'react';
import MuiSelect from '../base-controls/MuiSelect';
import { registerComponent } from 'meteor/vulcan:core';
const SelectMultiple = ({ refFunction, ...properties }) => {
properties.multiple = true;
return <MuiSelect {...properties} ref={refFunction}/>;
};
registerComponent('FormComponentSelectMultiple', SelectMultiple);

View file

@ -0,0 +1,15 @@
import React from 'react';
import MuiInput from '../base-controls/MuiInput';
import { registerComponent } from 'meteor/vulcan:core';
const TextareaComponent = ({ refFunction, ...properties }) =>
<MuiInput {...properties}
ref={refFunction}
multiline={true}
rows={properties.rows ? properties.rows : 2}
rowsMax={10}
/>;
registerComponent('FormComponentTextarea', TextareaComponent);

View file

@ -0,0 +1,37 @@
import React from 'react';
import MuiInput from '../base-controls/MuiInput';
import { registerComponent } from 'meteor/vulcan:core';
import withStyles from '@material-ui/core/styles/withStyles';
export const styles = theme => ({
'@global': {
'input[type=time]::-ms-clear, input[type=time]::-ms-reveal': {
display: 'none',
width: 0,
height: 0,
},
'input[type=time]::-webkit-search-cancel-button': {
display: 'none',
'-webkit-appearance': 'none',
},
'input[type="time"]::-webkit-clear-button': {
display: 'none',
'-webkit-appearance': 'none',
},
'input[type="time"]::-webkit-inner-spin-button,input[type="time"]::-webkit-outer-spin-button': {
'-webkit-appearance': 'none',
margin: 0,
},
},
});
const TimeComponent = ({ refFunction, classes, ...properties }) =>
<MuiInput {...properties} ref={refFunction} type="time"/>;
registerComponent('FormComponentTime', TimeComponent, [withStyles, styles]);

View file

@ -0,0 +1,73 @@
// Deprecated react-datetime version
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import DateTimePicker from 'react-datetime';
import { registerComponent } from 'meteor/vulcan:core';
class TimeRdt 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')});
}
}
render() {
const date = new Date();
// transform time string into date object to work inside datetimepicker
const time = this.props.value;
if (time) {
date.setHours(parseInt(time.substr(0,2)), parseInt(time.substr(3,5)));
} else {
date.setHours(0,0);
}
return (
<div className="form-group row">
<label className="control-label col-sm-3">{this.props.label}</label>
<div className="col-sm-9">
<DateTimePicker
value={date}
viewMode="time"
dateFormat={false}
timeFormat="HH:mm"
// newDate argument is a Moment object given by react-datetime
onChange={newDate => this.updateDate(newDate)}
inputProps={{name: this.props.name}}
/>
</div>
</div>
);
}
}
TimeRdt.propTypes = {
control: PropTypes.any,
datatype: PropTypes.any,
group: PropTypes.any,
label: PropTypes.string,
name: PropTypes.string,
value: PropTypes.any,
};
TimeRdt.contextTypes = {
updateCurrentValues: PropTypes.func,
};
export default TimeRdt;

View file

@ -0,0 +1,10 @@
import React from 'react';
import MuiInput from '../base-controls/MuiInput';
import { registerComponent } from 'meteor/vulcan:core';
const UrlComponent = ({ refFunction, ...properties }) =>
<MuiInput {...properties} ref={refFunction} type="url" />;
registerComponent('FormComponentUrl', UrlComponent);

View file

@ -0,0 +1,402 @@
export const countries = [
{ value: 'AF', label: 'Afghanistan' },
{ value: 'AX', label: 'Åland Islands' },
{ value: 'AL', label: 'Albania' },
{ value: 'DZ', label: 'Algeria' },
{ value: 'AS', label: 'American Samoa' },
{ value: 'AD', label: 'Andorra' },
{ value: 'AO', label: 'Angola' },
{ value: 'AI', label: 'Anguilla' },
{ value: 'AQ', label: 'Antarctica' },
{ value: 'AG', label: 'Antigua and Barbuda' },
{ value: 'AR', label: 'Argentina' },
{ value: 'AM', label: 'Armenia' },
{ value: 'AW', label: 'Aruba' },
{ value: 'AU', label: 'Australia' },
{ value: 'AT', label: 'Austria' },
{ value: 'AZ', label: 'Azerbaijan' },
{ value: 'BS', label: 'Bahamas' },
{ value: 'BH', label: 'Bahrain' },
{ value: 'BD', label: 'Bangladesh' },
{ value: 'BB', label: 'Barbados' },
{ value: 'BY', label: 'Belarus' },
{ value: 'BE', label: 'Belgium' },
{ value: 'BZ', label: 'Belize' },
{ value: 'BJ', label: 'Benin' },
{ value: 'BM', label: 'Bermuda' },
{ value: 'BT', label: 'Bhutan' },
{ value: 'BO', label: 'Bolivia' },
{ value: 'BQ', label: 'Bonaire, Sint Eustatius, Saba' },
{ value: 'BA', label: 'Bosnia and Herzegovina' },
{ value: 'BW', label: 'Botswana' },
{ value: 'BV', label: 'Bouvet Island' },
{ value: 'BR', label: 'Brazil' },
{ value: 'IO', label: 'British Indian Ocean Territory' },
{ value: 'BN', label: 'Brunei Darussalam' },
{ value: 'BG', label: 'Bulgaria' },
{ value: 'BF', label: 'Burkina Faso' },
{ value: 'BI', label: 'Burundi' },
{ value: 'CV', label: 'Cabo Verde' },
{ value: 'KH', label: 'Cambodia' },
{ value: 'CM', label: 'Cameroon' },
{ value: 'CA', label: 'Canada' },
{ value: 'KY', label: 'Cayman Islands' },
{ value: 'CF', label: 'Central African Republic' },
{ value: 'TD', label: 'Chad' },
{ value: 'CL', label: 'Chile' },
{ value: 'CN', label: 'China' },
{ value: 'CX', label: 'Christmas Island' },
{ value: 'CC', label: 'Cocos (Keeling) Islands' },
{ value: 'CO', label: 'Colombia' },
{ value: 'KM', label: 'Comoros' },
{ value: 'CG', label: 'Congo' },
{ value: 'CD', label: 'Congo (Democratic Republic of the)' },
{ value: 'CK', label: 'Cook Islands' },
{ value: 'CR', label: 'Costa Rica' },
{ value: 'CI', label: 'Côte dIvoire' },
{ value: 'HR', label: 'Croatia' },
{ value: 'CU', label: 'Cuba' },
{ value: 'CW', label: 'Curaçao' },
{ value: 'CY', label: 'Cyprus' },
{ value: 'CZ', label: 'Czechia' },
{ value: 'DK', label: 'Denmark' },
{ value: 'DJ', label: 'Djibouti' },
{ value: 'DM', label: 'Dominica' },
{ value: 'DO', label: 'Dominican Republic' },
{ value: 'EC', label: 'Ecuador' },
{ value: 'EG', label: 'Egypt' },
{ value: 'SV', label: 'El Salvador' },
{ value: 'GQ', label: 'Equatorial Guinea' },
{ value: 'ER', label: 'Eritrea' },
{ value: 'EE', label: 'Estonia' },
{ value: 'ET', label: 'Ethiopia' },
{ value: 'FK', label: 'Falkland Islands' },
{ value: 'FO', label: 'Faroe Islands' },
{ value: 'FJ', label: 'Fiji' },
{ value: 'FI', label: 'Finland' },
{ value: 'FR', label: 'France' },
{ value: 'GF', label: 'French Guiana' },
{ value: 'PF', label: 'French Polynesia' },
{ value: 'TF', label: 'French Southern Territories' },
{ value: 'GA', label: 'Gabon' },
{ value: 'GM', label: 'Gambia' },
{ value: 'GE', label: 'Georgia' },
{ value: 'DE', label: 'Germany' },
{ value: 'GH', label: 'Ghana' },
{ value: 'GI', label: 'Gibraltar' },
{ value: 'GR', label: 'Greece' },
{ value: 'GL', label: 'Greenland' },
{ value: 'GD', label: 'Grenada' },
{ value: 'GP', label: 'Guadeloupe' },
{ value: 'GU', label: 'Guam' },
{ value: 'GT', label: 'Guatemala' },
{ value: 'GG', label: 'Guernsey' },
{ value: 'GN', label: 'Guinea' },
{ value: 'GW', label: 'Guinea-Bissau' },
{ value: 'GY', label: 'Guyana' },
{ value: 'HT', label: 'Haiti' },
{ value: 'HM', label: 'Heard Island, Mcdonald Islands' },
{ value: 'VA', label: 'Vatican City State' },
{ value: 'HN', label: 'Honduras' },
{ value: 'HK', label: 'Hong Kong' },
{ value: 'HU', label: 'Hungary' },
{ value: 'IS', label: 'Iceland' },
{ value: 'IN', label: 'India' },
{ value: 'ID', label: 'Indonesia' },
{ value: 'IR', label: 'Iran' },
{ value: 'IQ', label: 'Iraq' },
{ value: 'IE', label: 'Ireland' },
{ value: 'IM', label: 'Isle of Man' },
{ value: 'IL', label: 'Israel' },
{ value: 'IT', label: 'Italy' },
{ value: 'JM', label: 'Jamaica' },
{ value: 'JP', label: 'Japan' },
{ value: 'JE', label: 'Jersey' },
{ value: 'JO', label: 'Jordan' },
{ value: 'KZ', label: 'Kazakhstan' },
{ value: 'KE', label: 'Kenya' },
{ value: 'KI', label: 'Kiribati' },
{ value: 'KW', label: 'Kuwait' },
{ value: 'KG', label: 'Kyrgyzstan' },
{ value: 'LA', label: 'Lao' },
{ value: 'LV', label: 'Latvia' },
{ value: 'LB', label: 'Lebanon' },
{ value: 'LS', label: 'Lesotho' },
{ value: 'LR', label: 'Liberia' },
{ value: 'LY', label: 'Libya' },
{ value: 'LI', label: 'Liechtenstein' },
{ value: 'LT', label: 'Lithuania' },
{ value: 'LU', label: 'Luxembourg' },
{ value: 'MO', label: 'Macao' },
{ value: 'MK', label: 'Macedonia' },
{ value: 'MG', label: 'Madagascar' },
{ value: 'MW', label: 'Malawi' },
{ value: 'MY', label: 'Malaysia' },
{ value: 'MV', label: 'Maldives' },
{ value: 'ML', label: 'Mali' },
{ value: 'MT', label: 'Malta' },
{ value: 'MH', label: 'Marshall Islands' },
{ value: 'MQ', label: 'Martinique' },
{ value: 'MR', label: 'Mauritania' },
{ value: 'MU', label: 'Mauritius' },
{ value: 'YT', label: 'Mayotte' },
{ value: 'MX', label: 'Mexico' },
{ value: 'FM', label: 'Micronesia' },
{ value: 'MD', label: 'Moldova' },
{ value: 'MC', label: 'Monaco' },
{ value: 'MN', label: 'Mongolia' },
{ value: 'ME', label: 'Montenegro' },
{ value: 'MS', label: 'Montserrat' },
{ value: 'MA', label: 'Morocco' },
{ value: 'MZ', label: 'Mozambique' },
{ value: 'MM', label: 'Myanmar' },
{ value: 'NA', label: 'Namibia' },
{ value: 'NR', label: 'Nauru' },
{ value: 'NP', label: 'Nepal' },
{ value: 'NL', label: 'Netherlands' },
{ value: 'NC', label: 'New Caledonia' },
{ value: 'NZ', label: 'New Zealand' },
{ value: 'NI', label: 'Nicaragua' },
{ value: 'NE', label: 'Niger' },
{ value: 'NG', label: 'Nigeria' },
{ value: 'NU', label: 'Niue' },
{ value: 'NF', label: 'Norfolk Island' },
{ value: 'KP', label: 'North Korea' },
{ value: 'MP', label: 'Northern Mariana Islands' },
{ value: 'NO', label: 'Norway' },
{ value: 'OM', label: 'Oman' },
{ value: 'PK', label: 'Pakistan' },
{ value: 'PW', label: 'Palau' },
{ value: 'PS', label: 'Palestine' },
{ value: 'PA', label: 'Panama' },
{ value: 'PG', label: 'Papua New Guinea' },
{ value: 'PY', label: 'Paraguay' },
{ value: 'PE', label: 'Peru' },
{ value: 'PH', label: 'Philippines' },
{ value: 'PN', label: 'Pitcairn' },
{ value: 'PL', label: 'Poland' },
{ value: 'PT', label: 'Portugal' },
{ value: 'PR', label: 'Puerto Rico' },
{ value: 'QA', label: 'Qatar' },
{ value: 'RE', label: 'Réunion' },
{ value: 'RO', label: 'Romania' },
{ value: 'RU', label: 'Russian Federation' },
{ value: 'RW', label: 'Rwanda' },
{ value: 'BL', label: 'Saint Barthélemy' },
{ value: 'SH', label: 'Saint Helena, Ascension, Tristan Da Cunha' },
{ value: 'KN', label: 'Saint Kitts and Nevis' },
{ value: 'LC', label: 'Saint Lucia' },
{ value: 'MF', label: 'Saint Martin (French Portion)' },
{ value: 'PM', label: 'Saint Pierre and Miquelon' },
{ value: 'VC', label: 'Saint Vincent and the Grenadines' },
{ value: 'WS', label: 'Samoa' },
{ value: 'SM', label: 'San Marino' },
{ value: 'ST', label: 'Sao Tome and Principe' },
{ value: 'SA', label: 'Saudi Arabia' },
{ value: 'SN', label: 'Senegal' },
{ value: 'RS', label: 'Serbia' },
{ value: 'SC', label: 'Seychelles' },
{ value: 'SL', label: 'Sierra Leone' },
{ value: 'SG', label: 'Singapore' },
{ value: 'SX', label: 'Sint Maarten (Dutch part)' },
{ value: 'SK', label: 'Slovakia' },
{ value: 'SI', label: 'Slovenia' },
{ value: 'SB', label: 'Solomon Islands' },
{ value: 'SO', label: 'Somalia' },
{ value: 'ZA', label: 'South Africa' },
{ value: 'GS', label: 'South Georgia, South Sandwich Islands' },
{ value: 'KR', label: 'South Korea' },
{ value: 'SS', label: 'South Sudan' },
{ value: 'ES', label: 'Spain' },
{ value: 'LK', label: 'Sri Lanka' },
{ value: 'SD', label: 'Sudan' },
{ value: 'SR', label: 'Suriname' },
{ value: 'SJ', label: 'Svalbard and Jan Mayen' },
{ value: 'SZ', label: 'Swaziland' },
{ value: 'SE', label: 'Sweden' },
{ value: 'CH', label: 'Switzerland' },
{ value: 'SY', label: 'Syria' },
{ value: 'TW', label: 'Taiwan' },
{ value: 'TJ', label: 'Tajikistan' },
{ value: 'TZ', label: 'Tanzania' },
{ value: 'TH', label: 'Thailand' },
{ value: 'TL', label: 'Timor-Leste' },
{ value: 'TG', label: 'Togo' },
{ value: 'TK', label: 'Tokelau' },
{ value: 'TO', label: 'Tonga' },
{ value: 'TT', label: 'Trinidad and Tobago' },
{ value: 'TN', label: 'Tunisia' },
{ value: 'TR', label: 'Turkey' },
{ value: 'TM', label: 'Turkmenistan' },
{ value: 'TC', label: 'Turks and Caicos Islands' },
{ value: 'TV', label: 'Tuvalu' },
{ value: 'UG', label: 'Uganda' },
{ value: 'UA', label: 'Ukraine' },
{ value: 'AE', label: 'United Arab Emirates' },
{ value: 'GB', label: 'United Kingdom' },
{ value: 'US', label: 'United States' },
{ value: 'UM', label: 'United States Minor Outlying Islands' },
{ value: 'UY', label: 'Uruguay' },
{ value: 'UZ', label: 'Uzbekistan' },
{ value: 'VU', label: 'Vanuatu' },
{ value: 'VE', label: 'Venezuela' },
{ value: 'VN', label: 'Viet Nam' },
{ value: 'VG', label: 'Virgin Islands, British' },
{ value: 'VI', label: 'Virgin Islands, U.S.' },
{ value: 'WF', label: 'Wallis and Futuna' },
{ value: 'EH', label: 'Western Sahara' },
{ value: 'YE', label: 'Yemen' },
{ value: 'ZM', label: 'Zambia' },
{ value: 'ZW', label: 'Zimbabwe' },
];
export const countryInfo = {
US: {
regionLabel: 'State',
postalLabel: 'Zip code',
regions: [
{ value: 'AL', label: 'Alabama' },
{ value: 'AK', label: 'Alaska' },
{ value: 'AZ', label: 'Arizona' },
{ value: 'AR', label: 'Arkansas' },
{ value: 'CA', label: 'California' },
{ value: 'CO', label: 'Colorado' },
{ value: 'CT', label: 'Connecticut' },
{ value: 'DE', label: 'Delaware' },
{ value: 'FL', label: 'Florida' },
{ value: 'GA', label: 'Georgia' },
{ value: 'HI', label: 'Hawaii' },
{ value: 'ID', label: 'Idaho' },
{ value: 'IL', label: 'Illinois' },
{ value: 'IN', label: 'Indiana' },
{ value: 'IA', label: 'Iowa' },
{ value: 'KS', label: 'Kansas' },
{ value: 'KY', label: 'Kentucky' },
{ value: 'LA', label: 'Louisiana' },
{ value: 'ME', label: 'Maine' },
{ value: 'MD', label: 'Maryland' },
{ value: 'MA', label: 'Massachusetts' },
{ value: 'MI', label: 'Michigan' },
{ value: 'MN', label: 'Minnesota' },
{ value: 'MS', label: 'Mississippi' },
{ value: 'MO', label: 'Missouri' },
{ value: 'MT', label: 'Montana' },
{ value: 'NE', label: 'Nebraska' },
{ value: 'NV', label: 'Nevada' },
{ value: 'NH', label: 'New Hampshire' },
{ value: 'NJ', label: 'New Jersey' },
{ value: 'NM', label: 'New Mexico' },
{ value: 'NY', label: 'New York' },
{ value: 'NC', label: 'North Carolina' },
{ value: 'ND', label: 'North Dakota' },
{ value: 'OH', label: 'Ohio' },
{ value: 'OK', label: 'Oklahoma' },
{ value: 'OR', label: 'Oregon' },
{ value: 'PA', label: 'Pennsylvania' },
{ value: 'RI', label: 'Rhode Island' },
{ value: 'SC', label: 'South Carolina' },
{ value: 'SD', label: 'South Dakota' },
{ value: 'TN', label: 'Tennessee' },
{ value: 'TX', label: 'Texas' },
{ value: 'UT', label: 'Utah' },
{ value: 'VT', label: 'Vermont' },
{ value: 'VA', label: 'Virginia' },
{ value: 'WA', label: 'Washington' },
{ value: 'WV', label: 'West Virginia' },
{ value: 'WI', label: 'Wisconsin' },
{ value: 'WY', label: 'Wyoming' },
]
},
CA: {
regionLabel: 'Province',
postalLabel: 'Postal code',
regions: [
{ value: 'AB', label: 'Alberta' },
{ value: 'BC', label: 'British Columbia' },
{ value: 'MB', label: 'Manitoba' },
{ value: 'NB', label: 'New Brunswick' },
{ value: 'NL', label: 'Newfoundland and Labrador' },
{ value: 'NS', label: 'Nova Scotia' },
{ value: 'NT', label: 'Northwest Territories' },
{ value: 'NU', label: 'Nunavut' },
{ value: 'ON', label: 'Ontario' },
{ value: 'PE', label: 'Prince Edward Island' },
{ value: 'QC', label: 'Quebec' },
{ value: 'SK', label: 'Saskatchewan' },
{ value: 'YT', label: 'Yukon' },
],
},
AU: {
regionLabel: 'State',
postalLabel: 'Postcode',
regions: [
{ value: 'ACT', label: 'Australian Capital Territory' },
{ value: 'NSW', label: 'New South Wales' },
{ value: 'NT', label: 'Northern Territory' },
{ value: 'QLD', label: 'Queensland' },
{ value: 'SA', label: 'South Australia' },
{ value: 'TAS', label: 'Tasmania' },
{ value: 'VIC', label: 'Victoria' },
{ value: 'WA', label: 'Western Australia' },
],
},
UK: {
regionLabel: 'County',
postalLabel: 'Postcode',
},
};
export const getCountryLabel = (countryValue) => {
const country = countries.find(country => country.value === countryValue);
return country ? country.label : '';
};
export const getCountryContinent = (countryValue) => {
const country = countries.find(country => country.value === countryValue);
return country ? country.continent : '';
};
export const getRegionLabel = (countryValue, regionValue) => {
if (!countryInfo[countryValue] || !countryInfo[countryValue].regions) {
return regionValue;
}
const regions = countryInfo[countryValue].regions;
let region = regions.find(nextRegion => nextRegion.value === regionValue);
if (region) {
return region.label;
} else {
return regionValue;
}
};
// Given a region value or label, returns the region value (QC or Quebec => QC)
// or false if the regionValue is invalid
export const validateRegion = (countryValue, regionValue) => {
if (!countryInfo[countryValue] || !countryInfo[countryValue].regions) {
return regionValue;
}
const regions = countryInfo[countryValue].regions;
let region = regions.find(nextRegion => nextRegion.value === regionValue);
if (region) {
return regionValue;
}
region = regions.find(nextRegion => nextRegion.label === regionValue);
if (region) {
return region.value;
} else {
return false;
}
};

View file

@ -0,0 +1,58 @@
import './accounts/AccountsButton';
import './accounts/AccountsButtons';
import './accounts/AccountsField';
import './accounts/AccountsFields';
import './accounts/AccountsForm';
import './accounts/AccountsPasswordOrService';
import './accounts/AccountsSocialButtons';
import './bonus/LoadMore';
import './bonus/SearchInput';
import './bonus/TooltipIntl';
import './bonus/TooltipIconButton';
import './core/Card';
import './core/Datatable';
import './core/EditButton';
import './core/Loading';
import './core/ModalTrigger';
import './core/NewButton';
import './forms/FormComponentInner';
import './forms/FormErrors';
import './forms/FormGroup';
import './forms/FormGroupNone';
import './forms/FormGroupWithLine';
import './forms/FormNested';
import './forms/FormNestedDivider';
import './forms/FormNestedFoot';
import './forms/FormNestedHead';
import './forms/FormSubmit';
import './forms/controls/Checkbox';
import './forms/controls/CheckboxGroup';
import './forms/controls/CountrySelect';
import './forms/controls/Date';
import './forms/controls/DateRdt';
import './forms/controls/DateTime';
import './forms/controls/DateTimeRdt';
import './forms/controls/Default';
import './forms/controls/Email';
import './forms/controls/Number';
import './forms/controls/PostalCode';
import './forms/controls/RadioGroup';
import './forms/controls/RegionSelect';
import './forms/controls/Select';
import './forms/controls/Textarea';
import './forms/controls/Time';
import './forms/controls/TimeRdt';
import './forms/controls/Url';
import './theme/ThemeStyles';
import './ui/Button';
import './ui/Alert';
import './upload/UploadImage';
import './upload/UploadInner';
export * from './forms/controls/countries';

View file

@ -0,0 +1,23 @@
import React, { PureComponent } from 'react';
class JssCleanup extends PureComponent {
// Remove the server-side injected CSS.
componentDidMount() {
if (!document || !document.getElementById) return;
const jssStyles = document.getElementById('jss-server-side');
if (jssStyles && jssStyles.parentNode) {
// jssStyles.parentNode.removeChild(jssStyles);
}
}
render() {
return this.props.children;
}
}
export default JssCleanup;

View file

@ -0,0 +1,232 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Components, registerComponent } from 'meteor/vulcan:core';
import withTheme from '@material-ui/core/styles/withTheme';
import withStyles from '@material-ui/core/styles/withStyles';
import Typography from '@material-ui/core/Typography';
import Grid from '@material-ui/core/Grid';
import Paper from '@material-ui/core/Paper';
import { getContrastRatio } from '@material-ui/core/styles/colorManipulator';
import classNames from 'classnames';
const describeTypography = (theme, className) => {
const typography = className ? theme.typography[className] : theme.typography;
const fontFamily = typography.fontFamily.split(',')[0];
return `${fontFamily} ${typography.fontWeight} ${typography.fontSize}px`;
};
const mainPalette = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900];
const altPalette = ['A100', 'A200', 'A400', 'A700'];
function getColorBlock(theme, classes, colorName, colorValue, colorTitle) {
const bgColor = theme.palette[colorName][colorValue];
let fgColor = theme.palette.common.black;
if (getContrastRatio(bgColor, fgColor) < 7) {
fgColor = theme.palette.common.white;
}
let blockTitle;
if (colorTitle) {
blockTitle = <div className={classes.name}>{colorName}</div>;
}
let rowStyle = {
backgroundColor: bgColor,
color: fgColor,
listStyle: 'none',
padding: 15,
};
if (colorValue.toString().indexOf('A1') === 0) {
rowStyle = {
...rowStyle,
marginTop: 4,
};
}
return (
<li style={rowStyle} key={colorValue}>
{blockTitle}
<div className={classes.colorContainer}>
<span>{colorValue}</span>
<span className={classes.colorValue}>{bgColor.toUpperCase()}</span>
</div>
</li>
);
}
function getColorGroup(options) {
const { theme, classes, color, showAltPalette } = options;
const cssColor = color.replace(' ', '').replace(color.charAt(0), color.charAt(0).toLowerCase());
let colorsList = [];
colorsList = mainPalette.map(mainValue => getColorBlock(theme, classes, cssColor, mainValue));
if (showAltPalette) {
altPalette.forEach(altValue => {
colorsList.push(getColorBlock(theme, classes, cssColor, altValue));
});
}
return (
<ul className={classes.colorGroup} key={cssColor}>
{getColorBlock(theme, classes, cssColor, 500, true)}
<div className={classes.blockSpace} />
{colorsList}
</ul>
);
}
const styles = theme => ({
root: {},
paper: {
padding: theme.spacing.unit * 3,
},
name: {
marginBottom: 60,
},
blockSpace: {
height: 4,
},
colorContainer: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
},
colorGroup: {
padding: '16px 0',
margin: '0 15px 0 0',
flexGrow: 1,
[theme.breakpoints.up('sm')]: {
flexGrow: 0,
},
},
colorValue: {
...theme.typography.caption,
color: 'inherit',
},
});
const latin = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur justo quam, ' +
'pellentesque ultrices ex a, aliquet porttitor ante. Donec tellus arcu, viverra ut lorem id, ' +
'ultrices ultricies enim. Donec enim metus, sollicitudin id lobortis id, iaculis ut arcu. ' +
'Maecenas sollicitudin congue nisi. Donec convallis, ipsum ac ultricies dignissim, orci ex ' +
'efficitur lectus, ac lacinia risus nunc at diam. Nam gravida bibendum lectus. Donec ' +
'scelerisque sem nec urna vestibulum vehicula.';
const ThemeStyles = ({ theme, classes }) => {
return (
<Grid container className={classNames('theme-styles', classes.root)}>
<Grid item xs={12}>
<Typography variant="h1">
h1: {describeTypography(theme, 'h1')}
</Typography>
<Typography variant="h2">
h2: {describeTypography(theme, 'h2')}
</Typography>
<Typography variant="h3">
h3: {describeTypography(theme, 'h3')}
</Typography>
<Typography variant="h4">
h4: {describeTypography(theme, 'h4')}
</Typography>
</Grid>
<Grid item xs={12}>
<Paper className={classes.paper}>
<Typography variant="h5" gutterBottom>
Headline: {describeTypography(theme, 'h5')}
</Typography>
<Typography variant="h6" gutterBottom>
Title: {describeTypography(theme, 'h6')}
</Typography>
<Typography variant="subtitle1" gutterBottom>
Subtitle1: {describeTypography(theme, 'subtitle1')}
</Typography>
<Typography variant="body1" gutterBottom>
Body 1: {describeTypography(theme, 'body1')} - {latin}
</Typography>
<Typography variant="body1" gutterBottom>
{latin}
</Typography>
<Typography variant="body2" gutterBottom>
Body 2: {describeTypography(theme, 'body2')} - {latin}
</Typography>
<Typography variant="body2" gutterBottom>
{latin}
</Typography>
<Typography variant="caption" gutterBottom align="center">
Caption: {describeTypography(theme, 'caption')}
</Typography>
<Typography gutterBottom>
Base: {describeTypography(theme)} - {latin}
</Typography>
<Typography variant="button" gutterBottom align="center">
Button - {describeTypography(theme)}
</Typography>
</Paper>
</Grid>
<Grid item xs={12} sm={6} md={3}>
{
getColorGroup({
theme,
classes,
color: 'primary',
showAltPalette: true,
})
}
</Grid>
<Grid item xs={12} sm={6} md={3}>
{
getColorGroup({
theme,
classes,
color: 'secondary',
showAltPalette: true,
})
}
</Grid>
<Grid item xs={12} sm={6} md={3}>
{
getColorGroup({
theme,
classes,
color: 'error',
showAltPalette: true,
})
}
</Grid>
<Grid item xs={12} sm={6} md={3}>
{
getColorGroup({
theme,
classes,
color: 'background',
showAltPalette: true,
})
}
</Grid>
</Grid>
);
};
ThemeStyles.propTypes = {
theme: PropTypes.object.isRequired,
classes: PropTypes.object.isRequired,
};
registerComponent('ThemeStyles', ThemeStyles, [withTheme, null], [withStyles, styles]);

View file

@ -0,0 +1,31 @@
/**
* @Author: Apollinaire Lecocq <apollinaire>
* @Date: 09-01-19
* @Last modified by: apollinaire
* @Last modified time: 10-01-19
*/
import React from 'react';
import withStyles from '@material-ui/core/styles/withStyles';
import { registerComponent } from 'meteor/vulcan:core';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
const AlertStyle = theme => ({
error: {
color: theme.palette.error.main,
backgroundColor: theme.palette.error[100],
fontFamily: theme.typography.fontFamily,
},
other: {
fontFamily: theme.typography.fontFamily,
},
});
const Alert = ({ children, variant, classes, ...rest }) => (
<Card className={variant === 'danger' ? classes.error : classes.other}>
<CardContent>{children}</CardContent>
</Card>
);
registerComponent({ name: 'Alert', component: Alert, hocs: [[withStyles, AlertStyle]] });

View file

@ -0,0 +1,51 @@
import React from 'react';
import { Components, registerComponent } from 'meteor/vulcan:core';
import MuiButton from '@material-ui/core/Button';
import MuiIconButton from '@material-ui/core/IconButton';
const Button = ({ children, color, variant, size, iconButton, ...rest }) => {
switch(variant) {
case 'success':
color = 'primary';
variant = null;
break;
case 'danger':
color = 'default';
variant = null;
break;
case 'inverse':
color = 'inherit';
variant = null;
break;
}
switch(size) {
case 'xsmall':
size = 'small';
break;
case 'small':
size = 'medium';
break;
case 'large':
size = 'large';
break;
}
if (iconButton) {
return (
<MuiIconButton color={color} variant={variant} size={size} {...rest}>
{children}
</MuiIconButton>
);
}
return (
<MuiButton color={color} variant={variant} size={size} {...rest}>
{children}
</MuiButton>
);
};
registerComponent('Button', Button);

View file

@ -0,0 +1,117 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { Components, registerComponent } from 'meteor/vulcan:lib';
import { FormattedMessage } from 'meteor/vulcan:i18n';
import withStyles from '@material-ui/core/styles/withStyles';
import IconButton from '@material-ui/core/IconButton';
import DeleteIcon from 'mdi-material-ui/Delete';
import classNames from 'classnames';
/**
* Used by UploadInner to display a single image
*/
const styles = theme => ({
uploadImage: {
textAlign: 'center',
marginBottom: theme.spacing.unit * -1,
marginLeft: theme.spacing.unit * 0.5,
marginRight: theme.spacing.unit * 0.5,
},
uploadImageContents: {
position: 'relative',
},
uploadImageImg: {
display: 'block',
maxWidth: 150,
maxHeight: 150,
},
uploadLoading: {
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
background: 'rgba(255,255,255,0.8)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
span: {
display: 'block',
fontSize: '1.5rem',
}
},
deleteButton: {
}
});
class UploadImage extends PureComponent {
constructor (props) {
super(props);
this.handleClear = this.handleClear.bind(this);
}
handleClear (event) {
event.preventDefault();
this.props.clearImage(this.props.index);
}
// Get the URL of an image or the first in an array of images
getImageUrl (imageOrImageArray) {
// if image is actually an array of formats, use first format
const image = Array.isArray(imageOrImageArray) ? imageOrImageArray[0] : imageOrImageArray;
// if image is an object, return secure_url; else return image itself
return typeof image === 'string' ? image : image.secure_url;
}
render () {
const { loading, error, image, style, classes } = this.props;
return (
<div className={classes.uploadImage}>
<div className={classes.uploadImageContents}>
<img className={classes.uploadImageImg} src={this.getImageUrl(image)} style={style}/>
{
loading &&
<div className={classes.uploadLoading}>
<Components.Loading/>
</div>
}
</div>
<IconButton className={classes.deleteButton} onClick={this.handleClear}>
<DeleteIcon/>
</IconButton>
</div>
);
}
}
UploadImage.propTypes = {
clearImage: PropTypes.func.isRequired,
index: PropTypes.number.isRequired,
image: PropTypes.oneOfType([PropTypes.string, PropTypes.array, PropTypes.object]),
loading: PropTypes.bool,
error: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
style: PropTypes.object,
classes: PropTypes.object.isRequired,
};
UploadImage.displayName = 'UploadImageMui';
registerComponent('UploadImage', UploadImage, [withStyles, styles]);

View file

@ -0,0 +1,181 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Components, registerComponent, getComponent } from 'meteor/vulcan:lib';
import Dropzone from 'react-dropzone';
import withStyles from '@material-ui/core/styles/withStyles';
import { FormattedMessage } from 'meteor/vulcan:i18n';
import FormControl from '@material-ui/core/FormControl';
import FormLabel from '@material-ui/core/FormLabel';
import FormHelperText from '@material-ui/core/FormHelperText';
/*
Material UI GUI for Cloudinary Image Upload component
*/
const styles = theme => ({
root: {},
label: {},
uploadField: {
marginTop: theme.spacing.unit,
},
dropzoneBase: {
borderWidth: 3,
borderStyle: 'dashed',
borderColor: theme.palette.background[900],
backgroundColor: theme.palette.background[100],
color: theme.palette.common.lightBlack,
padding: '30px 60px',
transition: 'all 0.5s',
cursor: 'pointer',
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
'&[aria-disabled="false"]:hover': {
color: theme.palette.common.midBlack,
borderColor: theme.palette.background['A200'],
}
},
dropzoneActive: {
borderStyle: 'solid',
borderColor: theme.palette.status.info,
},
dropzoneReject: {
borderStyle: 'solid',
borderColor: theme.palette.status.danger,
},
uploadState: {},
uploadImages: {
border: `1px solid ${theme.palette.background[500]}`,
backgroundColor: theme.palette.background[100],
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'center',
paddingTop: theme.spacing.unit,
paddingRight: theme.spacing.unit * 0.5,
paddingBottom: theme.spacing.unit,
paddingLeft: theme.spacing.unit * 0.5,
},
});
const UploadInner = (props) => {
const {
uploading,
images,
disabled,
maxCount,
label,
help,
options,
enableMultiple,
onDrop,
isDeleted,
clearImage,
classes
} = props;
const UploadImage = getComponent(options.uploadImageComponentName || 'UploadImage');
return (
<FormControl component="fieldset" fullWidth={true} className={classes.root}>
<FormLabel component="legend" className={classes.label}>
{label}
</FormLabel>
{
help &&
<FormHelperText>{help}</FormHelperText>
}
<div className={classes.uploadField}>
{
(disabled && !enableMultiple)
?
null
:
<Dropzone
style={options.dropzoneStyle}
multiple={enableMultiple}
onDrop={onDrop}
accept="image/*"
className={classes.dropzoneBase}
activeClassName={classes.dropzoneActive}
rejectClassName={classes.dropzoneReject}
disabled={disabled}
>
<div>
<FormattedMessage id={`upload.${disabled ? 'maxReached' : 'prompt'}`}
values={{ maxCount }}/>
</div>
{uploading && (
<div className="upload-uploading">
<span>
<FormattedMessage id={`upload.uploading`}/>
</span>
</div>
)}
</Dropzone>
}
{!!images.length && (
<div className={classes.uploadState}>
<div className={classes.uploadImages}>
{images.map(
(image, index) =>
!isDeleted(index) && (
<UploadImage
clearImage={clearImage}
key={index}
index={index}
image={image}
loading={image.loading}
preview={image.preview}
error={image.error}
style={options.imageStyle}
/>
)
)}
</div>
</div>
)}
</div>
</FormControl>
);
};
UploadInner.propTypes = {
uploading: PropTypes.bool,
images: PropTypes.array.isRequired,
disabled: PropTypes.bool,
maxCount: PropTypes.number.isRequired,
label: PropTypes.string,
help: PropTypes.string,
options: PropTypes.object.isRequired,
enableMultiple: PropTypes.bool,
onDrop: PropTypes.func.isRequired,
isDeleted: PropTypes.func.isRequired,
clearImage: PropTypes.func.isRequired,
classes: PropTypes.object.isRequired,
};
UploadInner.displayName = 'UploadInnerMui';
registerComponent('UploadInner', UploadInner, [withStyles, styles]);

View file

@ -0,0 +1,12 @@
import { addStrings } from 'meteor/vulcan:core';
addStrings('en', {
"search.search": "Search",
"search.clear": "Clear search",
"load_more.load_more": "Load more",
"load_more.loaded_count": "Loaded {count} of {totalCount}",
"load_more.loaded_all": "{totalCount, plural, =0 {No items} one {One item} other {# items}}",
});

View file

@ -0,0 +1,98 @@
import React from 'react';
import PropTypes from 'prop-types';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import IconButton from '@material-ui/core/IconButton';
import Typography from '@material-ui/core/Typography';
import MenuIcon from 'mdi-material-ui/Menu';
import ChevronLeftIcon from 'mdi-material-ui/ChevronLeft';
import withStyles from '@material-ui/core/styles/withStyles';
import { getSetting, Components, registerComponent } from 'meteor/vulcan:core';
import classNames from 'classnames';
const drawerWidth = 240;
const topBarHeight = 100;
const styles = theme => ({
appBar: {
position: 'absolute',
transition: theme.transitions.create(['margin', 'width'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
},
appBarShift: {
marginLeft: drawerWidth,
width: `calc(100% - ${drawerWidth}px)`,
transition: theme.transitions.create(['margin', 'width'], {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
},
toolbar: {
height: `${topBarHeight}px`,
minHeight: `${topBarHeight}px`,
},
headerMid: {
flexGrow: 1,
display: 'flex',
alignItems: 'center',
'& h1': {
margin: '0 24px 0 0',
fontSize: '18px',
lineHeight: 1,
}
},
menuButton: {
marginRight: theme.spacing.unit * 3,
},
});
const Header = (props, context) => {
const classes = props.classes;
const isSideNavOpen = props.isSideNavOpen;
const toggleSideNav = props.toggleSideNav;
const siteTitle = getSetting('title', 'My App');
return (
<AppBar className={classNames(classes.appBar, isSideNavOpen && classes.appBarShift)}>
<Toolbar className={classes.toolbar}>
<IconButton
aria-label="open drawer"
onClick={e => toggleSideNav()}
className={classNames(classes.menuButton)}
color="inherit"
>
{isSideNavOpen ? <ChevronLeftIcon/> : <MenuIcon/>}
</IconButton>
<div className={classNames(classes.headerMid)}>
<Typography variant="h6" color="inherit" className="tagline">
{siteTitle}
</Typography>
</div>
</Toolbar>
</AppBar>
);
};
Header.propTypes = {
classes: PropTypes.object.isRequired,
isSideNavOpen: PropTypes.bool,
toggleSideNav: PropTypes.func,
};
Header.displayName = 'Header';
registerComponent('Header', Header, [withStyles, styles]);

View file

@ -0,0 +1,136 @@
import React from 'react';
import PropTypes from 'prop-types';
import Drawer from '@material-ui/core/Drawer';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import { Components, replaceComponent, Utils } from 'meteor/vulcan:core';
import withStyles from '@material-ui/core/styles/withStyles';
import classNames from 'classnames';
const drawerWidth = 240;
const topBarHeight = 100;
const styles = theme => {
const contentPadding = theme.spacing.unit * 8;
return {
'@global': {
html: {
background: theme.palette.background.default,
WebkitFontSmoothing: 'antialiased',
MozOsxFontSmoothing: 'grayscale',
overflow: 'hidden',
},
body: {
margin: 0,
},
},
root: {
width: '100%',
zIndex: 1,
overflow: 'hidden',
},
appFrame: {
position: 'relative',
display: 'flex',
height: '100vh',
alignItems: 'stretch',
},
drawerPaper: {
position: 'relative',
width: drawerWidth,
backgroundColor: theme.palette.background[200],
},
drawerHeader: {
height: `${topBarHeight}px !important`,
minHeight: `${topBarHeight}px !important`,
position: 'relative !important',
},
content: {
padding: contentPadding,
width: '100%',
marginLeft: -drawerWidth,
flexGrow: 1,
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
height: `calc(100% - ${topBarHeight}px - ${contentPadding * 2}px)`,
marginTop: topBarHeight,
overflowY: 'scroll',
},
mainShift: {
marginLeft: 0,
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
},
};
};
class Layout extends React.Component {
state = {
isOpen: { sideNav: true }
};
toggle = (item, openOrClose) => {
const newState = { isOpen: {} };
newState.isOpen[item] = typeof openOrClose === 'string' ?
openOrClose === 'open' :
!this.state.isOpen[item];
this.setState(newState);
};
render = () => {
const routeName = Utils.slugify(this.props.currentRoute.name);
const classes = this.props.classes;
const isOpen = this.state.isOpen;
return (
<div className={classNames(classes.root, 'wrapper', `wrapper-${routeName}`)}>
<div className={classes.appFrame}>
<Components.Header isSideNavOpen={isOpen.sideNav}
toggleSideNav={openOrClose =>
this.toggle('sideNav', openOrClose)} />
<Drawer variant="persistent"
classes={{ paper: classes.drawerPaper, }}
open={isOpen.sideNav}
>
<AppBar className={classes.drawerHeader} elevation={4} square={true}>
<Toolbar>
</Toolbar>
</AppBar>
<Components.SideNavigation />
</Drawer>
<main className={classNames(classes.content, isOpen.sideNav && classes.mainShift)}>
{this.props.children}
</main>
<Components.FlashMessages />
</div>
</div>
);
};
}
Layout.propTypes = {
classes: PropTypes.object.isRequired,
children: PropTypes.node,
};
Layout.displayName = 'Layout';
replaceComponent('Layout', Layout, [withStyles, styles]);

View file

@ -0,0 +1,105 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Components, registerComponent, withCurrentUser } from 'meteor/vulcan:core';
import { withRouter } from 'react-router-dom';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import Divider from '@material-ui/core/Divider';
import Collapse from '@material-ui/core/Collapse';
import ExpandLessIcon from 'mdi-material-ui/ChevronUp';
import ExpandMoreIcon from 'mdi-material-ui/ChevronDown';
import LockIcon from 'mdi-material-ui/Lock';
import UsersIcon from 'mdi-material-ui/AccountMultiple';
import ThemeIcon from 'mdi-material-ui/Palette';
import HomeIcon from 'mdi-material-ui/Home';
import withStyles from '@material-ui/core/styles/withStyles';
import Users from 'meteor/vulcan:users';
const styles = theme => ({
root: {},
nested: {
paddingLeft: theme.spacing.unit * 4,
},
});
class SideNavigation extends React.Component {
state = {
isOpen: { admin: false }
};
toggle = (item) => {
const newState = { isOpen: {} };
newState.isOpen[item] = !this.state.isOpen[item];
this.setState(newState);
};
render () {
const currentUser = this.props.currentUser;
const classes = this.props.classes;
const isOpen = this.state.isOpen;
return (
<div className={classes.root}>
<List>
<ListItem button onClick={() => {this.props.history.push('/');}}>
<ListItemIcon>
<HomeIcon/>
</ListItemIcon>
<ListItemText inset primary="Home"/>
</ListItem>
</List>
{
Users.isAdmin(currentUser) &&
<div>
<Divider/>
<List>
<ListItem button onClick={e => this.toggle('admin')}>
<ListItemIcon>
<LockIcon/>
</ListItemIcon>
<ListItemText primary="Admin"/>
{isOpen.admin ? <ExpandLessIcon/> : <ExpandMoreIcon/>}
</ListItem>
<Collapse in={isOpen.admin} transitionduration="auto" unmountOnExit>
<ListItem button className={classes.nested}
onClick={() => {this.props.history.push('/admin');}}>
<ListItemIcon>
<UsersIcon/>
</ListItemIcon>
<ListItemText inset primary="Users"/>
</ListItem>
<ListItem button className={classes.nested}
onClick={() => {this.props.history.push('/theme');}}>
<ListItemIcon>
<ThemeIcon/>
</ListItemIcon>
<ListItemText inset primary="Theme"/>
</ListItem>
</Collapse>
</List>
</div>
}
</div>
);
}
}
SideNavigation.propTypes = {
classes: PropTypes.object.isRequired,
currentUser: PropTypes.object,
};
SideNavigation.displayName = 'SideNavigation';
registerComponent('SideNavigation', SideNavigation, [withStyles, styles], withCurrentUser, withRouter);

View file

@ -0,0 +1,16 @@
.form-nested-item {
display: flex;
}
.form-nested-item-inner {
flex-grow: 1;
}
.form-nested-item-remove {
padding-top: 8px;
margin-left: 8px;
}
.form-nested-item-remove > button {
margin-right: -4px;
}

View file

@ -0,0 +1,8 @@
import { addStrings } from 'meteor/vulcan:core';
addStrings('fr', {
"search.search": "Recherche",
"search.clear": "Effacer la recherche",
});

View file

@ -0,0 +1,109 @@
1.12.8_17 / 2019-02-02
======================
* TooltipIntl: Changed display from 'inline-block' to 'inherit' for more flexibility
* Countries: Added getRegionLabel function
1.12.8_16 / 2019-01-21
======================
* Countries: Fixed bug in validateRegion
1.12.8_15 / 2019-01-21
======================
* Countries: Fixed bug in validateRegion
1.12.8_14 / 2019-01-20
======================
* Countries: Added validateRegion function, which given a region value or label, will return the region value ('NY' or 'New York' => 'NY)
* The contents of countries is now exported - this may be refactored out of the core vulcan-material-ui as some point
1.12.8_13 / 2019-01-14
======================
* ModalTrigger: Added boolean dialogOverflow prop for use cases like popups that can go beyond the size of the dialog box
* MuiSuggest: Fixed bug - The disabled state was not displayed correctly
* MuiSuggest: Fixed bug - After selecting a suggestion, clicking on the control did not re-open the suggestions menu
1.12.8_12 / 2019-01-12
======================
* Upgraded to Meteor 1.8.0.2
1.12.8_11 / 2018-12-21
======================
* SearchInput: Added install autosize-input to readme
* Datatable: Fixed sorting delay
* Datatable: Added tableHeadCell class
* Datatable: Added cellClass column property, which can be a string or a function: column.cellClass({ column, document, currentUser })
1.12.8_10 / 2018-12-09
======================
* TooltipIntl: Added icon class
* FormGroupWithLine: Moved caret from the right side to next to the title
* Changed load_more.loaded_all string
1.12.8_9 / 2018-11-26
=====================
* Fixed bug that displayed invalid total count at the bottom of data tables
1.12.8_8 / 2018-11-23
=====================
* Improved the functionality of the LoadMore component
* The showNoMore property has been deprecated
* A showCount property has been added (true by default) that shows a count of loaded and total items
* The load more icon or button is displayed even when infiniteScroll is enabled
1.12.8_7 / 2018-11-10
=====================
* Fixed bug in Datatable.jsx
* Updated ReadMe
1.12.8_6 / 2018-11-06
=====================
* Fixed bug in Datatable.jsx
* Reduced spacing of form components
1.12.8_5 / 2018-10-31
=====================
* Fixed bugs in Datatable pagination
* Set Datatable paginate prop to false by default
1.12.8_4 / 2018-10-31
=====================
* Removed 'fr_FR.js' from package.js because any french strings loaded activates the french language
* Fixed delete button and its tooltips positioning in FormSubmit
* Added pagination to Datatable
1.12.8_2 / 2018-10-29
=====================
* Fixed localization in "clear search" tooltip
* Added name and aria-haspopup properties to the input component to improve compliance and facilitate UAT
* Replaced Date, Time and DateTime form controls with native controls as recommended by MUI.
The deprecated react-datetime version of the controls are still there as DateRdt, TimeRdt and DateTimeRdt, but they are not registered.
* Updated readme
1.12.8_1 / 2018-10-22
=====================
* Made form components compatible with new Form.formComponents property
1.12.8 / 2018-10-19
===================
* Made improvements to the search box, including keyboard shortcuts (s: focus search; c: clear search)
* Added support in TooltipIntl for tooltips in popovers
* Added action prop to ModalTrigger that enables a parent component to call openModal and closeModal
* Started using MUI tables in Card component
* Fixed bugs in MuiSuggest component

View file

@ -0,0 +1,4 @@
export * from './themes';
export JssCleanup from '../components/theme/JssCleanup';
import './sampleTheme';
import './routes';

View file

@ -0,0 +1,8 @@
import { addRoute } from 'meteor/vulcan:core';
addRoute({
name: 'theme',
path: '/theme',
componentName: 'ThemeStyles',
});

View file

@ -0,0 +1,76 @@
import { registerTheme } from './themes';
import indigo from '@material-ui/core/colors/indigo';
import blue from '@material-ui/core/colors/blue';
import red from '@material-ui/core/colors/red';
/** @ignore */
/**
*
* Sample theme to get you out of the gate quickly
*
* For a complete list of configuration variables see:
* https://material-ui.com/customization/themes/
*
*/
const theme = {
palette: {
primary: indigo,
secondary: blue,
error: red,
},
utils: {
tooltipEnterDelay: 700,
errorMessage: {
textAlign: 'center',
backgroundColor: red[500],
color: 'white',
borderRadius: '4px',
fontWeight: 'bold',
},
denseTable: {
'& > thead > tr > th, & > tbody > tr > td': {
padding: '4px 16px 4px 16px',
},
'& > thead > tr > th:last-child, & > tbody > tr > td:last-child': {
paddingRight: '16px',
},
},
flatTable: {
'& > thead > tr > th, & > tbody > tr > td': {
padding: '4px 16px 4px 16px',
whiteSpace: 'nowrap',
},
'& > thead > tr > th:last-child, & > tbody > tr > td:last-child': {
paddingRight: '16px',
},
},
denserTable: {
'& > thead > tr, & > tbody > tr': {
height: '40px',
},
'& > thead > tr > th, & > tbody > tr > td': {
padding: '4px 16px 4px 16px',
whiteSpace: 'nowrap',
},
'& > thead > tr > th:last-child, & > tbody > tr > td:last-child': {
paddingRight: '16px',
},
},
},
};
registerTheme('Sample', theme);

View file

@ -0,0 +1,67 @@
/** @module vulcan-material-ui */
import createMuiTheme from '@material-ui/core/styles/createMuiTheme';
import { registerSetting, getSetting } from 'meteor/vulcan:core';
registerSetting('muiTheme', 'Sample', 'Material UI theme used by erikdakota:vulcan-material-ui');
export const ThemesTable = {}; // storage for info about themes
/**
* Register a theme with a name
*
* @param {String} name The name of the theme to register
* @param {Object} theme The theme object - see defaultTheme.js
*
*/
export const registerTheme = (name, theme) => {
const themeInfo = {
name,
theme,
};
ThemesTable[name] = themeInfo;
};
/**
* Get a theme registered with registerTheme()
*
* @param {String} name The name of the theme to get
*
* @returns {Object} A theme object
*/
export const getTheme = (name) => {
const themeInfo = ThemesTable[name];
if (!themeInfo) return null;
themeInfo.theme.typography = { ...themeInfo.theme.typography, useNextVariants: true }
return createMuiTheme(themeInfo.theme);
};
/**
* Get the raw theme object registered with registerTheme()
*
* @param {String} name The name of the theme to get
*
* @returns {Object} The object passed to registerTheme
*/
export const getRawTheme = (name) => {
const themeInfo = ThemesTable[name];
if (!themeInfo) return null;
return themeInfo.theme;
}
/**
* Get the theme specified in the 'muiTheme' setting
*
* @returns {Object}
*/
export const getCurrentTheme = () => {
const themeName = getSetting('muiTheme', 'Sample');
const theme = getTheme(themeName);
return theme;
};

View file

@ -0,0 +1,26 @@
Package.describe({
name: 'vulcan:ui-material',
version: '1.12.17',
summary: 'Replacement for Vulcan (http://vulcanjs.org/) components using material-ui',
documentation: 'README.md'
});
Package.onUse(function (api) {
api.versionsFrom('METEOR@1.6');
api.use([
'ecmascript',
'vulcan:core@1.12.8',
'vulcan:accounts@1.12.8',
'vulcan:forms@1.12.8',
]);
api.addFiles([
'accounts.css',
'forms.css',
], ['client', 'server']);
api.mainModule('client/main.js', 'client');
api.mainModule('server/main.js', 'server');
});

View file

@ -0,0 +1,208 @@
# erikdakoda:vulcan-material-ui 1.12.8_13
Replacement for [Vulcan](http://vulcanjs.org/) components using [Material-UI](https://material-ui.com/).
This version has been tested against Vulcan 1.12.8 and Material-UI 3.1.0.
To give me feedback open an issue on GitHub or you can reach me on the [Vulcan Slack](https://vulcanjs.slack.com)
channel as erikdakoda.
There are some nice bonus features like a CountrySelect with autocomplete and theming.
All components in vulcan:ui-bootstrap, vulcan:forms and vulcan:accounts have been implemented except for Icon.
## Installation
To add vulcan-material-ui to an existing Vulcan project, enter the following:
``` sh
meteor add erikdakoda:vulcan-material-ui
meteor npm install --save @material-ui/core
meteor npm install --save react-jss
meteor npm install --save mdi-material-ui
meteor npm install --save react-autosuggest
meteor npm install --save autosuggest-highlight
meteor npm install --save react-isolated-scroll
meteor npm install --save-exact react-keyboard-event-handler@1.3.2
meteor npm install --save autosize-input
```
> NOTE: If you want to avoid deprecation warnings added in MUI versions after 3.1.0, you can lock MUI to the currently supported version using `meteor npm install --save @material-ui/core@3.1.0`. Don't for get to remove or update the version number when you update this package in the future.
> IMPORTANT: Please note that I have abandoned material-ui-icons in favor of mdi-material-ui because it has a much larger [selection of icons](https://materialdesignicons.com/).
This package no longer depends on `vulcan:ui-boostrap`, so you can remove it.
To activate the example layout copy the three components to your project and import them:
``` javascript
import './example/Header',
import './example/Layout',
import './example/SideNavigation',
```
## Theming
For an example theme see `modules/sampleTheme.js`. For a complete list of values you can customize,
see the [MUI Default Theme](https://material-ui-next.com/customization/default-theme/).
Register your theme in the Vulcan environment by giving it a name: `registerTheme('MyTheme', theme);`.
You can have multiple themes registered and you can specify which one to use in your settings file using the `muiTheme` public setting.
In addition to the Material UI spec, I use a `utils` section in my themes where I place global variables for reusable styles.
For example the sample theme contains
```
const theme = {
. . .
utils: {
tooltipEnterDelay: 700,
errorMessage: {
textAlign: 'center',
backgroundColor: red[500],
color: 'white',
borderRadius: '4px',
},
. . .
// additional utils definitions go here
},
};
```
You can use tooltipEnterDelay (or any other variable you define in utils) anywhere you include the withTheme HOC. See `/components/bonus/TooltipIconButton.jsx` for an example.
You can use errorMessage (or any other style fragment you define in utils) anywhere you include the withStyles HOC. See `/components/accounts/Form.jsx` for an example.
## Server Side Rendering (SSR)
Material UI and Vulcan support SSR, but this is a complex beast with pitfalls. Sometimes you will see a warning like this:
`Warning: Prop className did not match. Server: "MuiChip-label-131" Client: "MuiChip-label-130"`
Sometimes the React rendered on the server and the client don't match exactly and this causes a problem with [JSS](https://material-ui-next.com/customization/css-in-js/#jss). This is a complicated issue that has multiple causes and I will be working on solving each of the issues causing this over time.
Your pages should still render correctly, but there may be a blink and redraw when the first page after SSR loads in the browser.
In your own code, make sure that your components will render the same on the server and the client. This means not referring to client-side object such as `document` or `jQuery`. If you have a misbehaving component, try wrapping it with [react-no-ssr](https://github.com/kadirahq/react-no-ssr).
## Form Controls
You can pass a couple of extra options to inputs from the `form` property of your schema:
``` javascript
userKey: {
type: String,
label: 'User key',
description: 'The users key',
optional: true,
hidden: function ({ document }) {
return !document.platformId || !document.usePlatformApp;
},
inputProperties: {
autoFocus: true, // focus this input when the form loads
addonBefore: <KeyIcon/>, // adorn the start of the input
addonAfter: <KeyIcon/>, // adorn the end of the input
inputClassName: 'halfWidthLeft', // use 'halfWidthLeft' or 'halfWidthRight'
// to display two controls side by side
hideLabel: true, // hide the label
rows: 10, // for textareas you can specify the rows
variant: 'switch', // for checkboxgroups you can use either
// 'checkbox' (default) or 'switch'
inputProps: { step: 'any' } // Attributes applied to the input element, for
// ex pass the step attr to a number input
},
group: platformGroup,
canRead: ['members'],
canCreate: ['members'],
canUpdate: ['members'],
},
```
> Note: `form.getHidden` has been deprecated. Now you can just pass a function to `hidden`.
## Form Groups
You can pass a couple of extra options to form groups as well:
``` javascript
const platformGroup: {
name: 'shops.platform',
order: 4,
startComponent: 'ShopsPlatformTitle', // component to put at the top of the form group
endComponent: 'ShopsConnectButtons', // component to put at the bottom of the form group
},
```
## DataTable
You can pass the DataTable component an `editComponent` property in addition to or instead of `showEdit`. Here is a simplified example:
``` javascript
const AgendaJobActions = ({ collection, document }) => {
const scheduleAgent = () => {
Meteor.call('scheduleAgent', document.agentId);
};
return <Components.TooltipIconButton titleId="executions.execute_now"
icon={<Components.ExecutionsIcon/>}
onClick={scheduleAgent}/>;
};
AgendaJobActionsInner.propTypes = {
collecion: PropTypes.object.isRequired,
document: PropTypes.object.isRequired,
};
<Components.Datatable
editComponent={AgendaJobActions}
collection={AgendaJobs}
...
/>
```
You can also control the spacing of the table cells using the `dense` property. Valid values are:
| Value | Description |
| ------- | ------------ |
| dense | right cell padding of 16px instead of 56px |
| flat | right cell padding of 16px and nowrap |
| denser | right cell padding of 16px, nowrap, and row height of 40px instead of 56px |
You can also use other string values, as long as you define a `utils` entry named the same + `Table`, for example `myCustomTable`. Check out the sample theme for examples.
## CountrySelect
There is an additional component, an autosuggest-based country select.
``` javascript
country: {
type: String,
label: 'Country',
input: 'CountrySelect',
canRead: ['guests'],
canCreate: ['members'],
canUpdate: ['members'],
},
```
Countries are stored as their 2-letter country codes. I have included a helper function for displaying the country name:
``` javascript
import Typography from '@material-ui/core/Typography';
import { getCountryLabel } from 'meteor/erikdakoda:vulcan-material-ui';
<Typography variant="subtitle1">
{getCountryLabel(supplier.country)}
</Typography>
```

View file

@ -0,0 +1,3 @@
export * from '../components/index';
export * from '../modules/index';
import './wrapWithMuiTheme';

View file

@ -0,0 +1,43 @@
import React from 'react';
import { addCallback } from 'meteor/vulcan:core';
import JssProvider from 'react-jss/lib/JssProvider';
import MuiThemeProvider from '@material-ui/core/styles/MuiThemeProvider';
import createGenerateClassName from '@material-ui/core/styles/createGenerateClassName';
import { getCurrentTheme } from '../modules/themes';
import { SheetsRegistry } from 'react-jss/lib/jss';
import JssCleanup from '../components/theme/JssCleanup';
function wrapWithMuiTheme (app, {context }) {
const sheetsRegistry = new SheetsRegistry();
context.sheetsRegistry = sheetsRegistry;
const sheetsManager = new Map();
const theme = getCurrentTheme();
const generateClassName = createGenerateClassName({ seed: '' });
return (
<JssProvider registry={sheetsRegistry} generateClassName={generateClassName}>
<MuiThemeProvider theme={theme} sheetsManager={sheetsManager}>
<JssCleanup>
{app}
</JssCleanup>
</MuiThemeProvider>
</JssProvider>
);
}
function injectJss(sink, { context }) {
const sheets = context.sheetsRegistry.toString();
sink.appendToHead(
`<style id="jss-server-side">${sheets}</style>`
);
return sink;
}
addCallback('router.server.wrapper', wrapWithMuiTheme);
addCallback('router.server.postRender', injectJss);