Merge Apollo2 branch

This commit is contained in:
eric-burel 2019-01-02 17:18:54 +01:00
commit 022a72e41b
258 changed files with 4458 additions and 11839 deletions

View file

@ -52,7 +52,8 @@
1,
"single"
],
"react/prop-types": 0
"react/prop-types": 0,
"semi": [1, "always"]
},
"env": {
"browser": true,

17
.github/stale.yml vendored Normal file
View file

@ -0,0 +1,17 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

View file

@ -1 +1 @@
METEOR@1.7.0.5
METEOR@1.8

View file

@ -1,79 +1,82 @@
accounts-base@1.4.2
accounts-base@1.4.3
accounts-password@1.5.1
allow-deny@1.1.0
apollo@3.0.0
autoupdate@1.4.0
babel-compiler@7.1.1
babel-runtime@1.2.4
autoupdate@1.5.0
babel-compiler@7.2.1
babel-runtime@1.3.0
base64@1.0.11
binary-heap@1.0.10
binary-heap@1.0.11
blaze-tools@1.0.10
boilerplate-generator@1.5.0
boilerplate-generator@1.6.0
buffer@0.0.0
caching-compiler@1.1.12
caching-html-compiler@1.1.2
caching-compiler@1.2.0
caching-html-compiler@1.1.3
callback-hook@1.1.0
check@1.3.1
ddp@1.4.0
ddp-client@2.3.2
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.0
dynamic-import@0.4.2
ecmascript@0.11.1
dynamic-import@0.5.0
ecmascript@0.12.1
ecmascript-runtime@0.7.0
ecmascript-runtime-client@0.7.1
ecmascript-runtime-server@0.7.0
ecmascript-runtime-client@0.8.0
ecmascript-runtime-server@0.7.1
ejson@1.1.0
email@1.2.3
es5-shim@4.8.0
fourseven:scss@4.5.4
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
lmieulet:meteor-coverage@1.1.4
inter-process-messaging@0.1.0
lmieulet:meteor-coverage@2.0.2
localstorage@1.2.0
logging@1.1.20
meteor@1.9.0
meteor@1.9.2
meteorhacks:inject-initial@1.0.4
meteorhacks:picker@1.0.3
meteortesting:browser-tests@1.0.0
meteortesting:mocha@1.0.0
minifier-css@1.3.1
minifier-js@2.3.5
minimongo@1.4.4
modern-browsers@0.1.1
modules@0.12.2
modules-runtime@0.10.0
mongo@1.5.0
meteortesting:browser-tests@1.1.0
meteortesting:mocha@1.0.1
meteortesting:mocha-core@1.0.1
minifier-css@1.4.0
minifier-js@2.4.0
minimongo@1.4.5
modern-browsers@0.1.2
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-bcrypt@0.9.3
npm-mongo@3.0.7
npm-mongo@3.1.1
ordered-dict@1.1.0
percolatestudio:synced-cron@1.1.0
practicalmeteor:mocha-core@1.0.1
promise@0.11.1
random@1.1.0
rate-limit@1.0.9
reactive-var@1.0.11
reload@1.2.0
retry@1.1.0
routepolicy@1.0.13
routepolicy@1.1.0
server-render@0.3.1
service-configuration@1.0.11
sha@1.0.9
shell-server@0.3.1
shell-server@0.4.0
socket-stream-client@0.2.2
spacebars-compiler@1.1.3
srp@1.0.10
standard-minifier-css@1.4.1
standard-minifier-js@2.3.4
srp@1.0.12
standard-minifier-css@1.5.1
standard-minifier-js@2.4.0
static-html@1.2.2
templating-tools@1.1.2
tracker@1.2.0
@ -85,6 +88,7 @@ vulcan:email@1.12.8
vulcan:i18n@1.12.8
vulcan:i18n-en-us@1.12.8
vulcan:lib@1.12.8
vulcan:routing@1.12.8
vulcan:users@1.12.8
webapp@1.6.1
webapp@1.7.0
webapp-hashing@1.0.9

21
.prettierrc.js Normal file
View file

@ -0,0 +1,21 @@
'use strict';
const {esNextPaths} = require('./.vulcan/shared/pathsByLanguageVersion');
module.exports = {
bracketSpacing: false,
singleQuote: true,
jsxBracketSameLine: true,
trailingComma: 'es5',
printWidth: 80,
parser: 'babylon',
overrides: [
{
files: esNextPaths,
options: {
trailingComma: 'all',
},
},
],
};

76
.vulcan/prettier/index.js Normal file
View file

@ -0,0 +1,76 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
// Based on similar script in Jest
// https://github.com/facebook/jest/blob/a7acc5ae519613647ff2c253dd21933d6f94b47f/scripts/prettier.js
const chalk = require('chalk');
const glob = require('glob');
const prettier = require('prettier');
const fs = require('fs');
const listChangedFiles = require('../shared/listChangedFiles');
const prettierConfigPath = require.resolve('../../.prettierrc');
const mode = process.argv[2] || 'check';
const shouldWrite = mode === 'write' || mode === 'write-changed';
const onlyChanged = mode === 'check-changed' || mode === 'write-changed';
const changedFiles = onlyChanged ? listChangedFiles() : null;
let didWarn = false;
let didError = false;
const files = glob
.sync('**/*.js', {ignore: '**/node_modules/**'})
.filter(f => !onlyChanged || changedFiles.has(f));
if (!files.length) {
return;
}
files.forEach(file => {
const options = prettier.resolveConfig.sync(file, {
config: prettierConfigPath,
});
try {
const input = fs.readFileSync(file, 'utf8');
if (shouldWrite) {
const output = prettier.format(input, options);
if (output !== input) {
fs.writeFileSync(file, output, 'utf8');
}
} else {
if (!prettier.check(input, options)) {
if (!didWarn) {
console.log(
'\n' +
chalk.red(
` This project uses prettier to format all JavaScript code.\n`
) +
chalk.dim(` Please run `) +
chalk.reset('yarn prettier-all') +
chalk.dim(
` and add changes to files listed below to your commit:`
) +
`\n\n`
);
didWarn = true;
}
console.log(file);
}
}
} catch (error) {
didError = true;
console.log('\n\n' + error.message);
console.log(file);
}
});
if (didWarn || didError) {
process.exit(1);
}

View file

@ -0,0 +1,36 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const execFileSync = require('child_process').execFileSync;
const exec = (command, args) => {
console.log('> ' + [command].concat(args).join(' '));
const options = {
cwd: process.cwd(),
env: process.env,
stdio: 'pipe',
encoding: 'utf-8',
};
return execFileSync(command, args, options);
};
const execGitCmd = args =>
exec('git', args)
.trim()
.toString()
.split('\n');
const listChangedFiles = () => {
const mergeBase = execGitCmd(['merge-base', 'HEAD', 'devel']);
return new Set([
...execGitCmd(['diff', '--name-only', '--diff-filter=ACMRTUB', mergeBase]),
...execGitCmd(['ls-files', '--others', '--exclude-standard']),
]);
};
module.exports = listChangedFiles;

View file

@ -0,0 +1,18 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
// Files that are transformed and can use ES6/Flow/JSX.
const esNextPaths = [
// Internal forwarding modules
'packages/*/*.js',
'packages/*/*.jsx',
];
module.exports = {
esNextPaths,
};

View file

@ -50,18 +50,16 @@ You can find the even older, non-React version of Telescope on the [legacy](http
This project exists thanks to all the people who contribute.
<a href="graphs/contributors"><img src="https://opencollective.com/vulcan/contributors.svg?width=890&button=false" /></a>
<a href="https://github.com/VulcanJS/Vulcan/graphs/contributors"><img src="https://opencollective.com/vulcan/contributors.svg?width=890&button=false" /></a>
### Backers
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/vulcan#backer)]
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/vulcan#contribute)]
<a href="https://opencollective.com/vulcan#backers" target="_blank"><img src="https://opencollective.com/vulcan/backers.svg?width=890"></a>
<a href="https://opencollective.com/vulcan#contributors" target="_blank"><img src="https://opencollective.com/vulcan/backers.svg?width=890"></a>
### Sponsors
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/vulcan#sponsor)]
<a href="https://opencollective.com/vulcan/sponsor/0/website" target="_blank"><img src="https://opencollective.com/vulcan/sponsor/0/avatar.svg"></a>
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/vulcan#contribute)]
<a href="https://opencollective.com/vulcan#contributors" target="_blank"><img src="https://opencollective.com/vulcan/sponsors.svg?width=890"></a>

9718
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "Vulcan",
"version": "1.12.8",
"version": "1.12.13",
"engines": {
"npm": "^3.0"
},
@ -8,7 +8,15 @@
"start": "meteor --settings settings.json",
"lint": "eslint --cache --ext .jsx,js packages",
"test-unit": "TEST_WATCH=1 meteor test-packages ./packages/* --port 3002 --driver-package meteortesting:mocha --raw-logs",
"test": "npm run test-unit"
"test": "npm run test-unit",
"prettier": "node ./.vulcan/prettier/index.js write-changed",
"prettier-all": "node ./.vulcan/prettier/index.js write"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint",
"pre-push": "npm run prettier"
}
},
"dependencies": {
"@babel/runtime": "7.0.0-beta.55",
@ -17,12 +25,13 @@
"apollo-client": "^2.4.2",
"apollo-engine": "^0.5.4",
"apollo-errors": "^1.4.0",
"apollo-link-error": "^1.1.5",
"apollo-link-schema": "^1.1.1",
"apollo-link-state": "^0.4.2",
"apollo-server": "^2.1.0",
"apollo-server-express": "^2.1.0",
"babel-runtime": "^6.26.0",
"bcrypt": "^2.0.1",
"bcrypt": "^3.0.2",
"body-parser": "^1.18.2",
"chalk": "2.2.0",
"classnames": "^2.2.3",
@ -52,11 +61,10 @@
"import": "0.0.6",
"intl": "^1.2.4",
"intl-locales-supported": "^1.0.0",
"juice": "^1.11.0",
"juice": "^5.1.0",
"lodash": "^4.17.10",
"mailchimp": "^1.1.6",
"marked": "^0.3.9",
"metascraper": "^1.0.6",
"meteor-node-stubs": "^0.2.3",
"mingo": "^2.2.0",
"moment": "^2.13.0",
@ -64,7 +72,7 @@
"react": "^16.2.0",
"react-addons-pure-render-mixin": "^15.4.1",
"react-apollo": "^2.2.4",
"react-bootstrap": "^0.32.0",
"react-bootstrap": "^1.0.0-beta.3",
"react-bootstrap-datetimepicker": "0.0.22",
"react-cookie": "^2.1.4",
"react-datetime": "^2.11.1",
@ -85,7 +93,6 @@
"redux": "^3.6.0",
"rss": "^1.2.1",
"sanitize-html": "^1.16.3",
"sendy-api": "^0.1.0",
"simpl-schema": "^1.4.2",
"speakingurl": "^9.0.0",
"stripe": "^4.23.1",
@ -98,9 +105,10 @@
"devDependencies": {
"autoprefixer": "^6.3.6",
"babel-eslint": "^7.0.0",
"babylon": "^6.18.0",
"chromedriver": "^2.40.0",
"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
"enzyme-adapter-react-16.3": "^1.4.0",
"eslint": "^3.19.0",
"eslint-config-airbnb": "^13.0.0",
"eslint-config-meteor": "0.0.9",
@ -113,8 +121,11 @@
"eslint-plugin-prettier": "^2.5.0",
"eslint-plugin-react": "^6.7.1",
"expect": "^23.4.0",
"glob": "^7.1.3",
"husky": "^1.2.0",
"jsdom": "^11.11.0",
"jsdom-global": "^3.0.2",
"prettier": "^1.15.2",
"selenium-webdriver": "^4.0.0-alpha.1"
},
"postcss": {

View file

@ -1,11 +1,11 @@
Package.describe({
summary: "Generates the boilerplate html from program's manifest",
version: '1.5.0',
summary: 'Generates the boilerplate html from program\'s manifest',
version: '1.6.0',
name: 'boilerplate-generator'
});
Npm.depends({
"combined-stream2": "1.1.2"
'combined-stream2': '1.1.2'
});
Package.onUse(api => {

View file

@ -0,0 +1,12 @@
import {Accounts} from 'meteor/accounts-base';
import {getSetting} from 'meteor/vulcan:core';
// the emailTemplates are made available by accounts-password, which we don't want to depend on
if (Package['accounts-password']) {
Accounts.emailTemplates.siteName = getSetting('public.title', '');
Accounts.emailTemplates.from =
getSetting('public.title', '') +
' <' +
getSetting('defaultEmail', 'no-reply@example.com') +
'>';
}

View file

@ -80,7 +80,7 @@ export function validatePassword(password = '', showMessage, clearMessage){
if (password.length >= Accounts.ui._options.minimumPasswordLength) {
return true;
} else {
const errMsg = 'accounts.error_minchar'
const errMsg = 'accounts.error_minchar';
showMessage(errMsg, 'warning', false, 'password');
return false;
}

View file

@ -29,7 +29,7 @@ class AccountsEnrollAccount extends PureComponent {
AccountsEnrollAccount.contextTypes = {
intl: intlShape
}
};
AccountsEnrollAccount.propsTypes = {
currentUser: PropTypes.object,

View file

@ -7,7 +7,7 @@ const autocompleteValues = {
'usernameOrEmail': 'email',
'email': 'email',
'password': 'current-password'
}
};
export class AccountsField extends PureComponent {
constructor(props) {
@ -75,4 +75,4 @@ AccountsField.propTypes = {
onChange: PropTypes.func
};
registerComponent('AccountsField', AccountsField)
registerComponent('AccountsField', AccountsField);

View file

@ -35,9 +35,9 @@ export class AccountsLoginFormInner extends TrackerComponent {
return () => {
props.client.resetStore().then(() => {
hook();
})
}
}
});
};
};
const postLogInAndThen = hook => {
return () => {
@ -47,9 +47,9 @@ export class AccountsLoginFormInner extends TrackerComponent {
} else { // or else execute the hook
hook();
}
})
}
}
});
};
};
const doNothing = () => {};
@ -1008,16 +1008,16 @@ export class AccountsLoginFormInner extends TrackerComponent {
AccountsLoginFormInner.propTypes = {
showSignInLink: PropTypes.bool,
showSignUpLink: PropTypes.bool,
}
};
AccountsLoginFormInner.defaultProps = {
showSignInLink: true,
showSignUpLink: true,
redirect: true,
}
};
AccountsLoginFormInner.contextTypes = {
intl: intlShape
}
};
registerComponent('AccountsLoginFormInner', AccountsLoginFormInner, withCurrentUser, withApollo);

View file

@ -29,7 +29,7 @@ class AccountsResetPassword extends PureComponent {
AccountsResetPassword.contextTypes = {
intl: intlShape
}
};
AccountsResetPassword.propsTypes = {
currentUser: PropTypes.object,

View file

@ -13,7 +13,7 @@ export class AccountsStateSwitcher extends React.Component {
this.state = {
formState: props.formState
}
};
}
switchToSignUp = (event) => {
@ -92,7 +92,7 @@ export class AccountsStateSwitcher extends React.Component {
switchToSignOut,
cancelResetPassword,
switchToProfile,
}
};
return (
<Components.AccountsLoginFormInner {...this.props} formState={this.state.formState} handlers={handlers} />
);

View file

@ -21,11 +21,11 @@ class TrackerComponent extends React.Component {
autorun(fn) { return this.__comps.push(Tracker.autorun(c => {
this.__live = true; fn(c); this.__live = false;
}))}
}));}
componentDidUpdate() { !this.__live && this.__comps.forEach(c => {
c.invalidated = c.stopped = false; !c.invalidate();
})}
});}
subscriptionsReady() {
return !Object.keys(this.__subs).some(id => !this.__subs[id].ready());

View file

@ -6,11 +6,11 @@ import { intlShape } from 'meteor/vulcan:i18n';
class AccountsVerifyEmail extends PureComponent {
constructor(props) {
super(props)
super(props);
this.state = {
pending: true,
error: null
}
};
}
componentDidMount() {
@ -32,7 +32,7 @@ class AccountsVerifyEmail extends PureComponent {
render() {
if(this.state.pending) {
return <Components.Loading />
return <Components.Loading />;
} else if(this.state.error) {
return (
<div className='password-reset-form'>
@ -51,7 +51,7 @@ class AccountsVerifyEmail extends PureComponent {
AccountsVerifyEmail.contextTypes = {
intl: intlShape
}
};
AccountsVerifyEmail.propsTypes = {
currentUser: PropTypes.object,

View file

@ -4,6 +4,7 @@ import './imports/components.js';
import './imports/login_session.js';
import './imports/routes.js';
import './imports/oauth_config.js';
import './imports/emailTemplates.js';
import { redirect, STATES } from './imports/helpers.js';
import './imports/api/server/servicesListPublication.js';

View file

@ -1,6 +1,6 @@
Package.describe({
name: 'vulcan:accounts',
version: '1.12.8',
version: '1.12.13',
summary: 'Accounts UI for React in Meteor 1.3+',
git: 'https://github.com/studiointeract/accounts-ui',
documentation: 'README.md'
@ -9,7 +9,7 @@ Package.describe({
Package.onUse(function(api) {
api.versionsFrom('1.6.1');
api.use('vulcan:core@1.12.8');
api.use('vulcan:core@1.12.13');
api.use('ecmascript');
api.use('tracker');

View file

@ -19,6 +19,6 @@ const AdminHome = ({ currentUser }) =>
showEdit={true}
/>
</Components.ShowIf>
</div>
</div>;
export default withCurrentUser(AdminHome);

View file

@ -9,14 +9,14 @@ const AdminUsersActions = ({ document: user, removeMutation }) =>{
if (confirm(`Delete user ${Users.getDisplayName(user)}?`)) {
removeMutation({documentId: user._id});
}
}
};
return <Components.Button variant="primary" onClick={deleteHandler}>Delete</Components.Button>
}
return <Components.Button variant="primary" onClick={deleteHandler}>Delete</Components.Button>;
};
const removeOptions = {
collection: Users
}
};
export default withRemove(removeOptions)(AdminUsersActions);

View file

@ -5,6 +5,6 @@ import moment from 'moment';
const AdminUsersCreated = ({ document: user }) =>
<div>
{moment(new Date(user.createdAt)).format('MM/DD/YY')}
</div>
</div>;
export default AdminUsersCreated;

View file

@ -3,6 +3,6 @@ import Users from 'meteor/vulcan:users';
import { Components } from 'meteor/vulcan:core';
const AdminUsersEmail = ({ document: user }) =>
<a href={`mailto:${Users.getEmail(user)}`}>{Users.getEmail(user)}</a>
<a href={`mailto:${Users.getEmail(user)}`}>{Users.getEmail(user)}</a>;
export default AdminUsersEmail;

View file

@ -13,6 +13,6 @@ const AdminUsersName = ({ document: user, flash }) =>
{_.rest(Users.getGroups(user)).map(group => <code key={group}>{group}</code>)}
</div>
</div>;
export default AdminUsersName;

View file

@ -1,7 +1,7 @@
Package.describe({
name: 'vulcan:admin',
summary: 'Vulcan components package',
version: '1.12.8',
version: '1.12.13',
git: 'https://github.com/VulcanJS/Vulcan.git'
});
@ -11,10 +11,10 @@ Package.onUse(function (api) {
api.use([
'fourseven:scss@4.5.0',
'fourseven:scss@4.10.0',
'dynamic-import@0.1.1',
// Vulcan packages
'vulcan:core@1.12.8',
'vulcan:core@1.12.13',
]);

View file

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

View file

@ -2,4 +2,4 @@ import { addCustomFields } from '../modules/index.js';
export const makeCloudinary = ({collection, fieldName}) => {
addCustomFields(collection);
}
};

View file

@ -41,7 +41,7 @@ export const addCustomFields = collection => {
type: 'String',
arguments: 'format: String',
resolver: (document, {format}, context) => {
const image = format ? _.findWhere(document.cloudinaryUrls, {name: format}) : document.cloudinaryUrls[0]
const image = format ? _.findWhere(document.cloudinaryUrls, {name: format}) : document.cloudinaryUrls[0];
return image && image.url;
}
},
@ -50,4 +50,4 @@ export const addCustomFields = collection => {
]);
}
};

View file

@ -1,3 +1,3 @@
export * from './cloudinary.js';
export * from '../modules/index.js';
export * from './make_cloudinary.js'
export * from './make_cloudinary.js';

View file

@ -39,4 +39,4 @@ export const makeCloudinary = ({collection, fieldName}) => {
}
addCallback(`${collection.options.collectionName.toLowerCase()}.edit.sync`, cacheImageOnEdit);
}
};

View file

@ -1,7 +1,7 @@
Package.describe({
name: 'vulcan:cloudinary',
summary: 'Vulcan file upload package.',
version: '1.12.8',
version: '1.12.13',
git: 'https://github.com/VulcanJS/Vulcan.git'
});
@ -10,7 +10,7 @@ Package.onUse(function (api) {
api.versionsFrom('1.6.1');
api.use([
'vulcan:core@1.12.8'
'vulcan:core@1.12.13'
]);
api.mainModule('lib/client/main.js', 'client');

View file

@ -1,16 +1,15 @@
import { addCallback, getActions } from 'meteor/vulcan:lib';
/*
Core callbacks
Core callbacks
*/
/**
* @summary Clear flash messages marked as seen when the route changes
* @param {Object} Item needed by `runCallbacks` to iterate on, unused here
* @param {Object} Redux store reference instantiated on the current connected client
* @param {Object} Apollo Client reference instantiated on the current connected client
* @param {Object} props
* @param {Object} props.client Apollo Client reference instantiated on the current connected client
*/
function RouterClearMessages(unusedItem, nextRoute, store, apolloClient) {
// TODO Apollo2: clear error messages on route change
@ -18,4 +17,4 @@ function RouterClearMessages(unusedItem, nextRoute, store, apolloClient) {
// return unusedItem;
}
addCallback('router.onUpdate', RouterClearMessages);
addCallback('router.onUpdate.async', RouterClearMessages);

View file

@ -13,6 +13,7 @@ import PropTypes from 'prop-types';
import { IntlProvider, intlShape } from 'meteor/vulcan:i18n';
import withCurrentUser from '../containers/withCurrentUser.js';
import withUpdate from '../containers/withUpdate.js';
import withSiteData from '../containers/withSiteData.js';
import { withApollo } from 'react-apollo';
import { withCookies } from 'react-cookie';
import moment from 'moment';
@ -34,31 +35,47 @@ const RouteWithLayout = ({ layoutName, component, ...rest }) => (
/>
);
const DummyErrorCatcher = ({ children }) => children;
class App extends PureComponent {
constructor(props) {
super(props);
if (props.currentUser) {
runCallbacks('events.identify', props.currentUser);
}
const locale = this.initLocale();
this.state = { locale };
const { locale, localeMethod } = this.initLocale();
this.state = { locale, localeMethod };
moment.locale(locale);
}
componentDidMount() {
runCallbacks('app.mounted', this.props);
}
initLocale = () => {
let userLocale = '';
const { currentUser, cookies } = this.props;
let localeMethod = '';
const { currentUser, cookies, locale } = this.props;
const availableLocales = Object.keys(Strings);
const detectedLocale = detectLocale();
if (currentUser && currentUser.locale) {
// 1. if user is logged in, check for their preferred locale
userLocale = currentUser.locale;
if (locale) {
// 1. locale is passed through SSR process
// TODO: currently SSR locale is passed through cookies as a hack
userLocale = locale;
localeMethod = 'SSR';
} else if (cookies && cookies.get('locale')) {
// 2. else, look for a cookie
// 2. look for a cookie
userLocale = cookies.get('locale');
} else if (detectLocale()) {
// 3. else, check for browser settings
userLocale = detectLocale();
localeMethod = 'cookie';
} else if (currentUser && currentUser.locale) {
// 3. if user is logged in, check for their preferred locale
userLocale = currentUser.locale;
localeMethod = 'user';
} else if (detectedLocale) {
// 4. else, check for browser settings
userLocale = detectedLocale;
localeMethod = 'browser';
}
// if user locale is available, use it; else compare first two chars
// of user locale with first two chars of available locales
@ -67,23 +84,29 @@ class App extends PureComponent {
: availableLocales.find(locale => locale.slice(0, 2) === userLocale.slice(0, 2));
// 4. if user-defined locale is available, use it; else default to setting or `en-US`
return availableLocale ? availableLocale : getSetting('locale', 'en-US');
if (availableLocale) {
return { locale: availableLocale, localeMethod };
} else {
return { locale: getSetting('locale', 'en-US'), localeMethod: 'setting' };
}
};
getLocale = (truncate = false) => {
getLocale = truncate => {
return truncate ? this.state.locale.slice(0, 2) : this.state.locale;
};
setLocale = async locale => {
const { cookies, updateUser, client, currentUser } = this.props;
this.setState({ locale });
this.props.cookies.set('locale', locale);
cookies.remove('locale', { path: '/' });
cookies.set('locale', locale, { path: '/' });
// if user is logged in, change their `locale` profile property
if (this.props.currentUser) {
await this.props.updateUser({ selector: { documentId: this.props.currentUser._id }, data: { locale } });
if (currentUser) {
await updateUser({ selector: { documentId: currentUser._id }, data: { locale } });
}
moment.locale(locale);
if (hasIntlFields) {
this.props.client.resetStore();
client.resetStore();
}
};
@ -106,6 +129,12 @@ class App extends PureComponent {
render() {
const routeNames = Object.keys(Routes);
//const currentRoute = _.last(this.props.routes);
//const LayoutComponent = currentRoute.layoutName ? Components[currentRoute.layoutName] : Components.Layout;
//
// // if defined, use ErrorCatcher component to wrap layout contents
// const ErrorCatcher = Components.ErrorCatcher ? Components.ErrorCatcher : DummyErrorCatcher;
return (
<IntlProvider locale={this.getLocale()} key={this.getLocale()} messages={Strings[this.getLocale()]}>
<div className={`locale-${this.getLocale()}`}>
@ -121,11 +150,23 @@ class App extends PureComponent {
// because it is the direct child of Switch
<RouteWithLayout exact key={key} {...Routes[key]} />
))}
<Route component={Components.Error404} /> // TODO Apollo2: figure out why this is not working
<Route component={Components.Error404} />
</Switch>
) : (
<Components.Welcome />
)}
{/*
<Components.RouterHook currentRoute={currentRoute} />
<LayoutComponent {...this.props} currentRoute={currentRoute}>
{this.props.currentUserLoading ? (
<Components.Loading />
) : this.props.children ? (
<ErrorCatcher siteData={this.props.siteData}>{this.props.children}</ErrorCatcher>
) : (
<Components.Welcome />
)}
</LayoutComponent>
*/}
</div>
</IntlProvider>
);
@ -149,6 +190,6 @@ const updateOptions = {
fragmentName: 'UsersCurrent',
};
registerComponent('App', App, withCurrentUser, [withUpdate, updateOptions], withApollo, withCookies);
registerComponent('App', App, withCurrentUser, withSiteData, [withUpdate, updateOptions], withApollo, withCookies);
export default App;

View file

@ -10,7 +10,7 @@ const Avatar = ({ className, user, link, fallback }) => {
const avatarClassNames = classNames('avatar', className);
if (!user) {
return <div className={avatarClassNames}>{fallback}</div>
return <div className={avatarClassNames}>{fallback}</div>;
}
const avatarUrl = user.avatarUrl || Users.avatar.getUrl(user);
@ -30,18 +30,18 @@ const Avatar = ({ className, user, link, fallback }) => {
</div>
);
}
};
Avatar.propTypes = {
user: PropTypes.object,
size: PropTypes.string,
link: PropTypes.bool
}
};
Avatar.defaultProps = {
size: 'medium',
link: true
}
};
Avatar.displayName = 'Avatar';

View file

@ -14,7 +14,7 @@ const getLabel = (field, fieldName, collection, intl) => {
} else {
return fieldName;
}
}
};
const getTypeName = (field, fieldName, collection) => {
const schema = collection && collection.simpleSchema()._schema;
@ -26,18 +26,18 @@ const getTypeName = (field, fieldName, collection) => {
} 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%', minWidth: 80, maxWidth: 200, display: 'block' }} src={value} alt={value} /> :
parseUrl(value);
}
};
const parseUrl = value => {
return value.slice(0, 4) === 'http' ? <a href={value} target="_blank"><LimitedString string={value} /></a> : <LimitedString string={value} />;
}
return value.slice(0,4) === 'http' ? <a href={value} target="_blank"><LimitedString string={value}/></a> : <LimitedString string={value}/>;
};
const LimitedString = ({ string }) =>
<div>
@ -45,12 +45,12 @@ const LimitedString = ({ string }) =>
<span title={string}>{string.substr(0, 30)}</span> :
<span>{(string)}</span>
}
</div>
</div>;
export const getFieldValue = (value, typeName) => {
if (typeof value === 'undefined' || value === null) {
return ''
return '';
}
// JSX element
@ -76,7 +76,7 @@ export const getFieldValue = (value, typeName) => {
return <code>{value.toString()}</code>;
case 'Array':
return <ol>{value.map((item, index) => <li key={index}>{getFieldValue(item, typeof item)}</li>)}</ol>
return <ol>{value.map((item, index) => <li key={index}>{getFieldValue(item, typeof item)}</li>)}</ol>;
case 'Object':
case 'object':
@ -92,7 +92,7 @@ export const getFieldValue = (value, typeName) => {
default:
return value.toString();
}
}
};
const getObject = object => {
@ -105,7 +105,7 @@ const getObject = object => {
<Components.Avatar size="small" user={user} link />
<Link to={user.pageUrl}>{user.displayName}</Link>
</div>
)
);
} else {
@ -120,16 +120,16 @@ const getObject = object => {
)}
</tbody>
</table>
)
);
}
}
};
const CardItem = ({ label, value, typeName }) =>
<tr>
<td className="datacard-label"><strong>{label}</strong></td>
<td className="datacard-value">{getFieldValue(value, typeName)}</td>
</tr>
</tr>;
const CardEdit = (props, context) =>
<tr>
@ -138,7 +138,7 @@ const CardEdit = (props, context) =>
<CardEditForm {...props} />
</Components.ModalTrigger>
</td>
</tr>
</tr>;
CardEdit.contextTypes = { intl: intlShape };
@ -150,7 +150,7 @@ const CardEditForm = ({ collection, document, closeModal }) =>
successCallback={document => {
closeModal();
}}
/>
/>;
const Card = ({ title, className, collection, document, currentUser, fields, showEdit = true }, { intl }) => {
@ -181,10 +181,10 @@ Card.propTypes = {
currentUser: PropTypes.object,
fields: PropTypes.array,
showEdit: PropTypes.bool
}
};
Card.contextTypes = {
intl: intlShape
}
};
registerComponent('Card', Card);

View file

@ -1,10 +1,12 @@
import { registerComponent, Components, getCollection, Utils } from 'meteor/vulcan:lib';
import { registerComponent, getCollection, Utils } from 'meteor/vulcan:lib';
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import withCurrentUser from '../containers/withCurrentUser.js';
import withComponents from '../containers/withComponents';
import withMulti from '../containers/withMulti.js';
import { FormattedMessage, intlShape } from 'meteor/vulcan:i18n';
import { getFieldValue } from './Card.jsx';
import _sortBy from 'lodash/sortBy';
/*
@ -21,6 +23,12 @@ const delay = (function(){
};
})();
const getColumnName = column => (
typeof column === 'string'
? column
: column.label || column.name
);
class Datatable extends PureComponent {
constructor() {
@ -30,7 +38,7 @@ class Datatable extends PureComponent {
value: '',
query: '',
currentSort: {}
}
};
}
toggleSort = column => {
@ -46,7 +54,7 @@ class Datatable extends PureComponent {
}
updateQuery(e) {
e.persist()
e.persist();
e.preventDefault();
this.setState({
value: e.target.value
@ -59,10 +67,11 @@ class Datatable extends PureComponent {
}
render() {
const { Components } = this.props;
if (this.props.data) { // static JSON datatable
return <Components.DatatableContents columns={Object.keys(this.props.data[0])} {...this.props} results={this.props.data} showEdit={false} showNew={false} />;
return <Components.DatatableContents Components={Components} columns={Object.keys(this.props.data[0])} {...this.props} results={this.props.data} showEdit={false} showNew={false} />;
} else { // dynamic datatable with data loading
@ -70,18 +79,23 @@ class Datatable extends PureComponent {
const options = {
collection,
...this.props.options
}
};
const DatatableWithMulti = withMulti(options)(Components.DatatableContents);
const canInsert = collection.options && collection.options.mutations && collection.options.mutations.new && collection.options.mutations.new.check(this.props.currentUser);
// 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={`datatable datatable-${collection.options.collectionName}`}>
<Components.DatatableAbove {...this.props} collection={collection} canInsert={canInsert} value={this.state.value} updateQuery={this.updateQuery} />
<DatatableWithMulti {...this.props} collection={collection} terms={{query: this.state.query, orderBy: this.state.currentSort }} currentUser={this.props.currentUser} toggleSort={this.toggleSort} currentSort={this.state.currentSort}/>
</div>
)
<Components.DatatableLayout Components={Components} collectionName={collection.options.collectionName}>
<Components.DatatableAbove Components={Components} {...this.props} collection={collection} canInsert={canInsert} value={this.state.value} updateQuery={this.updateQuery} />
<DatatableWithMulti Components={Components} {...this.props} collection={collection} terms={{ query: this.state.query, orderBy: orderBy }} currentUser={this.props.currentUser} toggleSort={this.toggleSort} currentSort={this.state.currentSort}/>
</Components.DatatableLayout>
);
}
}
}
@ -98,41 +112,80 @@ Datatable.propTypes = {
newFormOptions: PropTypes.object,
editFormOptions: PropTypes.object,
emptyState: PropTypes.object,
}
Components: PropTypes.object.isRequired,
};
Datatable.defaultProps = {
showNew: true,
showEdit: true,
showSearch: true,
}
registerComponent('Datatable', Datatable, withCurrentUser);
};
registerComponent({ name: 'Datatable', component: Datatable, hocs: [withCurrentUser, withComponents] });
export default Datatable;
const DatatableLayout = ({ collectionName, children }) => (
<div className={`datatable datatable-${collectionName}`}>
{children}
</div>
);
registerComponent({ name: 'DatatableLayout', component: DatatableLayout });
/*
DatatableAbove Component
*/
const DatatableAbove = (props) => {
const { collection, currentUser, showSearch, showNew, canInsert, value, updateQuery, options, newFormOptions } = props;
const DatatableAbove = (props, { intl }) => {
const { collection, currentUser, showSearch, showNew, canInsert,
value, updateQuery, options, newFormOptions, Components } = props;
return (
<div className="datatable-above">
{showSearch && <input className="datatable-search form-control" placeholder="Search…" type="text" name="datatableSearchQuery" value={value} onChange={updateQuery} />}
<Components.DatatableAboveLayout>
{showSearch && (
<Components.DatatableAboveSearchInput
className="datatable-search form-control"
placeholder={`${intl.formatMessage({ id: 'datatable.search', defaultMessage: 'Search' })}…`}
type="text"
name="datatableSearchQuery"
value={value}
onChange={updateQuery}
/>
)}
{showNew && canInsert && <Components.NewButton collection={collection} currentUser={currentUser} mutationFragmentName={options && options.fragmentName} {...newFormOptions}/>}
</div>
)
}
</Components.DatatableAboveLayout>
);
};
DatatableAbove.contextTypes = {
intl: intlShape,
};
DatatableAbove.propTypes = {
Components: PropTypes.object.isRequired
};
registerComponent('DatatableAbove', DatatableAbove);
const DatatableAboveSearchInput = (props) => (
<input
{...props}
/>
);
registerComponent({ name: 'DatatableAboveSearchInput', component: DatatableAboveSearchInput });
const DatatableAboveLayout = ({ children }) => (
<div className="datatable-above">
{children}
</div>
);
registerComponent({ name: 'DatatableAboveLayout', component: DatatableAboveLayout });
/*
DatatableHeader Component
*/
const DatatableHeader = ({ collection, column, toggleSort, currentSort }, { intl }) => {
const DatatableHeader = ({ collection, column, toggleSort, currentSort, Components }, { intl }) => {
const columnName = typeof column === 'string' ? column : column.label || column.name;
const columnName = getColumnName(column);
if (collection) {
const schema = collection.simpleSchema()._schema;
@ -151,43 +204,55 @@ const DatatableHeader = ({ collection, column, toggleSort, currentSort }, { intl
// 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;
return column.sortable ? <Components.DatatableSorter name={sortPropertyName} label={formattedLabel} toggleSort={toggleSort} currentSort={currentSort} sortable={column.sortable}/> : <th>{formattedLabel}</th>;
return column.sortable
? <Components.DatatableSorter name={sortPropertyName} label={formattedLabel} toggleSort={toggleSort} currentSort={currentSort} sortable={column.sortable}/>
: <Components.DatatableHeaderCellLayout>{formattedLabel}</Components.DatatableHeaderCellLayout>;
} else {
const formattedLabel = intl.formatMessage({ id: columnName, defaultMessage: columnName });
return <th className={`datatable-th-${columnName.toLowerCase().replace(/\s/g,'-')}`}>{formattedLabel}</th>;
return (
<Components.DatatableHeaderCellLayout
className={`datatable-th-${columnName.toLowerCase().replace(/\s/g, '-')}`}
>
{formattedLabel}
</Components.DatatableHeaderCellLayout>
);
}
}
};
DatatableHeader.contextTypes = {
intl: intlShape
};
DatatableHeader.propTypes = {
Components: PropTypes.object.isRequired
};
registerComponent('DatatableHeader', DatatableHeader);
const DatatableHeaderCellLayout = ({ children, ...otherProps }) => (
<th {...otherProps}>{children}</th>
);
registerComponent({ name: 'DatatableHeaderCellLayout', component: DatatableHeaderCellLayout });
const SortNone = () =>
<svg width='16' height='16' viewBox='0 0 438 438' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path d='M25.7368 247.243H280.263C303.149 247.243 314.592 274.958 298.444 291.116L171.18 418.456C161.128 428.515 144.872 428.515 134.926 418.456L7.55631 291.116C-8.59221 274.958 2.85078 247.243 25.7368 247.243ZM298.444 134.884L171.18 7.54408C161.128 -2.51469 144.872 -2.51469 134.926 7.54408L7.55631 134.884C-8.59221 151.042 2.85078 178.757 25.7368 178.757H280.263C303.149 178.757 314.592 151.042 298.444 134.884Z' transform='translate(66 6)' fill='#000' fillOpacity='0.2' />
</svg>
</svg>;
const SortDesc = () =>
<svg width="16" height="16" viewBox="0 0 438 438" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25.7368 0H280.263C303.149 0 314.592 27.7151 298.444 43.8734L171.18 171.213C161.128 181.272 144.872 181.272 134.926 171.213L7.55631 43.8734C-8.59221 27.7151 2.85078 0 25.7368 0Z" transform="translate(66 253.243)" fill="black" fillOpacity="0.7"/>
<path d="M171.18 7.54408L298.444 134.884C314.592 151.042 303.149 178.757 280.263 178.757H25.7368C2.85078 178.757 -8.59221 151.042 7.55631 134.884L134.926 7.54408C144.872 -2.51469 161.128 -2.51469 171.18 7.54408Z" transform="translate(66 6)" fill="black" fillOpacity="0.2"/>
</svg>
</svg>;
const SortAsc = () =>
<svg width="16" height="16" viewBox="0 0 438 438" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M298.444 134.884L171.18 7.54408C161.128 -2.51469 144.872 -2.51469 134.926 7.54408L7.55631 134.884C-8.59221 151.042 2.85078 178.757 25.7368 178.757H280.263C303.149 178.757 314.592 151.042 298.444 134.884Z" transform="translate(66 6)" fill="black" fillOpacity="0.7"/>
<path d="M280.263 0H25.7368C2.85078 0 -8.59221 27.7151 7.55631 43.8734L134.926 171.213C144.872 181.272 161.128 181.272 171.18 171.213L298.444 43.8734C314.592 27.7151 303.149 0 280.263 0Z" transform="translate(66 253.243)" fill="black" fillOpacity="0.2"/>
</svg>
</svg>;
const DatatableSorter = ({ name, label, toggleSort, currentSort }) =>
<th>
<div className="datatable-sorter" onClick={() => {toggleSort(name)}}>
<div className="datatable-sorter" onClick={() => {toggleSort(name);}}>
<span className="datatable-sorter-label">{label}</span>
<span className="sort-icon">
{!currentSort[name] ? (
@ -200,7 +265,7 @@ const DatatableSorter = ({ name, label, toggleSort, currentSort }) =>
}
</span>
</div>
</th>
</th>;
registerComponent('DatatableSorter', DatatableSorter);
@ -213,7 +278,10 @@ DatatableContents Component
const DatatableContents = (props) => {
// if no columns are provided, default to using keys of first array item
const { title, collection, results, columns, loading, loadMore, count, totalCount, networkStatus, showEdit, currentUser, emptyState, toggleSort, currentSort } = props;
const { title, collection, results, columns, loading, loadMore,
count, totalCount, networkStatus, showEdit, currentUser, emptyState,
toggleSort, currentSort,
Components } = props;
if (loading) {
return <div className="datatable-list datatable-list-loading"><Components.Loading /></div>;
@ -223,41 +291,89 @@ const DatatableContents = (props) => {
const isLoadingMore = networkStatus === 2;
const hasMore = totalCount > results.length;
const sortedColumns = _sortBy(columns, column => column.order);
return (
<div className="datatable-list">
<Components.DatatableContentsLayout>
{title && <Components.DatatableTitle title={title}/>}
<table className="table">
<thead>
<tr>
{_.sortBy(columns, column => column.order).map((column, index) => <Components.DatatableHeader key={index} collection={collection} column={column} toggleSort={toggleSort} currentSort={currentSort} />)}
{showEdit ? <th><FormattedMessage id="datatable.edit"/></th> : null}
</tr>
</thead>
<tbody>
{results.map((document, index) => <Components.DatatableRow {...props} collection={collection} columns={columns} document={document} key={index} showEdit={showEdit} currentUser={currentUser}/>)}
</tbody>
</table>
{hasMore &&
<div className="datatable-list-load-more">
{isLoadingMore ?
<Components.Loading/> :
<Components.Button variant="primary" onClick={e => {e.preventDefault(); loadMore();}}>Load More ({count}/{totalCount})</Components.Button>
<Components.DatatableContentsInnerLayout>
<Components.DatatableContentsHeadLayout>
{
sortedColumns
.map((column, index) => (
<Components.DatatableHeader
Components={Components}
key={index} collection={collection} column={column}
toggleSort={toggleSort} currentSort={currentSort} />)
)
}
</div>
{showEdit ? <th><FormattedMessage id="datatable.edit" /></th> : null}
</Components.DatatableContentsHeadLayout>
<Components.DatatableContentsBodyLayout>
{results.map((document, index) => <Components.DatatableRow {...props} collection={collection} columns={columns} document={document} key={index} showEdit={showEdit} currentUser={currentUser} />)}
</Components.DatatableContentsBodyLayout>
</Components.DatatableContentsInnerLayout>
{hasMore &&
<Components.DatatableContentsMoreLayout>
{isLoadingMore
? <Components.Loading />
: (
<Components.DatatableLoadMoreButton Components={Components} onClick={e => { e.preventDefault(); loadMore(); }}>
Load More ({count}/{totalCount})
</Components.DatatableLoadMoreButton>
)
}
</Components.DatatableContentsMoreLayout>
}
</div>
)
}
</Components.DatatableContentsLayout>
);
};
DatatableContents.propTypes = {
Components: PropTypes.object.isRequired
};
registerComponent('DatatableContents', DatatableContents);
const DatatableContentsLayout = ({ children }) => (
<div className="datatable-list">
{children}
</div>
);
registerComponent({ name: 'DatatableContentsLayout', component: DatatableContentsLayout });
const DatatableContentsInnerLayout = ({ children }) => (
<table className="table">
{children}
</table>
);
registerComponent({ name: 'DatatableContentsInnerLayout', component: DatatableContentsInnerLayout });
const DatatableContentsHeadLayout = ({ children }) => (
<thead>
<tr>
{children}
</tr>
</thead>
);
registerComponent({ name: 'DatatableContentsHeadLayout', component: DatatableContentsHeadLayout });
const DatatableContentsBodyLayout = ({ children }) => (
<tbody>{children}</tbody>
);
registerComponent({ name: 'DatatableContentsBodyLayout', component: DatatableContentsBodyLayout });
const DatatableContentsMoreLayout = ({ children }) => (
<div className="datatable-list-load-more">
{children}
</div>
);
registerComponent({ name: 'DatatableContentsMoreLayout', component: DatatableContentsMoreLayout });
const DatatableLoadMoreButton = ({ count, totalCount, Components, children, ...otherProps }) => (
<Components.Button variant="primary" {...otherProps}>{children}</Components.Button>
);
registerComponent({ name: 'DatatableLoadMoreButton', component: DatatableLoadMoreButton });
/*
DatatableTitle Component
*/
const DatatableTitle = ({ title }) =>
<div className="datatable-title">{title}</div>
<div className="datatable-title">{title}</div>;
registerComponent('DatatableTitle', DatatableTitle);
@ -268,52 +384,81 @@ DatatableRow Component
*/
const DatatableRow = (props, { intl }) => {
const { collection, columns, document, showEdit, currentUser, options, editFormOptions, rowClass } = props;
const { collection, columns, document, showEdit,
currentUser, options, editFormOptions, rowClass, Components } = props;
const canEdit = collection && collection.options && collection.options.mutations && collection.options.mutations.edit && collection.options.mutations.edit.check(currentUser, document);
const row = typeof rowClass === 'function' ? rowClass(document) : rowClass || '';
const modalProps = { title: <code>{document._id}</code> };
const sortedColumns = _sortBy(columns, column => column.order);
return (
<tr className={`datatable-item ${row}`}>
{_.sortBy(columns, column => column.order).map((column, index) => <Components.DatatableCell key={index} column={column} document={document} currentUser={currentUser} />)}
<Components.DatatableRowLayout className={`datatable-item ${row}`}>
{
sortedColumns
.map((column, index) => (
<Components.DatatableCell
key={index}
Components={Components}
column={column} document={document}
currentUser={currentUser} />
))}
{showEdit && canEdit ?
<td>
<Components.DatatableCellLayout>
<Components.EditButton collection={collection} documentId={document._id} currentUser={currentUser} mutationFragmentName={options && options.fragmentName} modalProps={modalProps} {...editFormOptions}/>
</td>
</Components.DatatableCellLayout>
: null}
</tr>
)
}
</Components.DatatableRowLayout>
);
};
DatatableRow.propTypes = {
Components: PropTypes.object.isRequired
};
registerComponent('DatatableRow', DatatableRow);
DatatableRow.contextTypes = {
intl: intlShape
};
const DatatableRowLayout = ({ children, ...otherProps }) => (
<tr {...otherProps}>
{children}
</tr>
);
registerComponent({ name: 'DatatableRowLayout', component: DatatableRowLayout });
/*
DatatableCell Component
*/
const DatatableCell = ({ column, document, currentUser }) => {
const Component = column.component || column.componentName && Components[column.componentName] || Components.DatatableDefaultCell;
const columnName = column.name || column;
const DatatableCell = ({ column, document, currentUser, Components }) => {
const Component = column.component
|| column.componentName && Components[column.componentName]
|| Components.DatatableDefaultCell;
const columnName = getColumnName(column);
return (
<td className={`datatable-item-${columnName.toLowerCase().replace(/\s/g,'-')}`}><Component column={column} document={document} currentUser={currentUser} /></td>
)
}
<Components.DatatableCellLayout className={`datatable-item-${columnName.toLowerCase().replace(/\s/g, '-')}`}>
<Component column={column} document={document} currentUser={currentUser} />
</Components.DatatableCellLayout>
);
};
DatatableCell.propTypes = {
Components: PropTypes.object.isRequired
};
registerComponent('DatatableCell', DatatableCell);
const DatatableCellLayout = ({ children, ...otherProps }) => (
<td {...otherProps}>{children}</td>
);
registerComponent({ name: 'DatatableCellLayout', component: DatatableCellLayout });
/*
DatatableDefaultCell Component
*/
const DatatableDefaultCell = ({ column, document }) =>
<div>{typeof column === 'string' ? getFieldValue(document[column]) : getFieldValue(document[column.name])}</div>
<div>{typeof column === 'string' ? getFieldValue(document[column]) : getFieldValue(document[column.name])}</div>;
registerComponent('DatatableDefaultCell', DatatableDefaultCell);

View file

@ -11,7 +11,7 @@ const DynamicLoading = ({ isLoading, pastDelay, error }) => {
} else {
return null;
}
}
};
registerComponent('DynamicLoading', DynamicLoading);

View file

@ -32,15 +32,15 @@ EditForm Component
const EditForm = ({ closeModal, successCallback, removeSuccessCallback, ...props }) => {
const success = successCallback
? () => {
successCallback();
? document => {
successCallback(document);
closeModal();
}
: closeModal;
const remove = removeSuccessCallback
? () => {
removeSuccessCallback();
? document => {
removeSuccessCallback(document);
closeModal();
}
: closeModal;

View file

@ -7,8 +7,8 @@ const Error404 = () => {
<div className="error404">
<h3><FormattedMessage id="app.404"/></h3>
</div>
)
}
);
};
Error404.displayName = 'Error404';

View file

@ -11,7 +11,7 @@ class Flash extends PureComponent {
}
componentDidMount() {
this.props.markAsSeen(this.props.message._id);
this.props.markAsSeen && this.props.markAsSeen(this.props.message._id);
}
dismissFlash(e) {
@ -42,25 +42,20 @@ class Flash extends PureComponent {
};
render() {
const { message, type } = this.getProperties();
const { message, type = 'danger' } = this.getProperties();
const flashType = type === 'error' ? 'danger' : type; // if flashType is "error", use "danger" instead
return (
<Components.Alert
className="flash-message"
variant={flashType}
onDismiss={this.dismissFlash}
>
{message}
<Components.Alert className="flash-message" variant={flashType} onDismiss={this.dismissFlash}>
<span dangerouslySetInnerHTML={{ __html: message }} />
</Components.Alert>
);
}
}
Flash.propTypes = {
message: PropTypes.object.isRequired,
markAsSeen: PropTypes.func.isRequired,
clear: PropTypes.func.isRequired
message: PropTypes.oneOfType([PropTypes.object.isRequired, PropTypes.string.isRequired])
};
Flash.contextTypes = {
@ -69,17 +64,12 @@ Flash.contextTypes = {
registerComponent('Flash', Flash);
const FlashMessages = ({ messages, clear, markAsSeen }) => {
const FlashMessages = ({messages, clear, markAsSeen, className}) => {
return (
<div className="flash-messages">
{messages.filter(message => message.show).map(message => (
<Components.Flash
key={message._id}
message={message}
clear={clear}
markAsSeen={markAsSeen}
/>
))}
<div className={`flash-messages ${className}`}>
{messages
.filter(message => message.show)
.map(message => <Components.Flash key={message._id} message={message} clear={clear} markAsSeen={markAsSeen} />)}
</div>
);
};

View file

@ -75,7 +75,7 @@ class HeadTags extends PureComponent {
} else {
HeadComponent = componentOrArray;
}
return <HeadComponent key={index} />
return <HeadComponent key={index} />;
})}
</div>

View file

@ -13,18 +13,18 @@ const wrapper = {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}
};
const header = {
textAlign: 'center',
}
};
const code = {
border: '1px solid #ccc',
borderRadius: 3,
padding: '10px 20px',
background: 'white',
}
};
function escapeHtml(unsafe) {
return unsafe
@ -73,7 +73,7 @@ addRoute({ name: 'home', path: '/', componentName: 'Home' });
</div>
</div>
</div>;
HelloWorld.displayName = 'HelloWorld';

View file

@ -7,7 +7,7 @@ const Icon = ({ name, iconClass, onClick }) => {
iconClass = (typeof iconClass === 'string') ? ' '+iconClass : '';
const c = 'icon fa fa-fw fa-' + iconCode + ' icon-' + name + iconClass;
return <i onClick={onClick} className={c} aria-hidden="true"></i>;
}
};
Icon.displayName = 'Icon';

View file

@ -2,7 +2,7 @@ import { Components, registerComponent } from 'meteor/vulcan:lib';
import React from 'react';
const Layout = ({children}) =>
<div className="wrapper" id="wrapper">{children}</div>
<div className="wrapper" id="wrapper">{children}</div>;
Layout.displayName = 'Layout';

View file

@ -8,8 +8,8 @@ const Loading = props => {
<div className="bounce2"></div>
<div className="bounce3"></div>
</div>
)
}
);
};
Loading.displayName = 'Loading';

View file

@ -32,6 +32,7 @@ class MutationButtonInner extends PureComponent {
successCallback(result);
}
}).catch(error => {
this.setState({ loading: false });
if(errorCallback) {
errorCallback(error);
}
@ -46,6 +47,8 @@ class MutationButtonInner extends PureComponent {
delete rest[mutationName];
delete rest.mutationOptions;
delete rest.mutationArguments;
delete rest.successCallback;
delete rest.errorCallback;
return <Components.LoadingButton loading={loading} onClick={this.handleClick} label={label} {...rest}/>;
}
@ -80,6 +83,6 @@ const LoadingButton = ({ loading, label, onClick, children, ...rest }) => {
</span>
</Components.Button>
);
}
};
registerComponent('LoadingButton', LoadingButton);

View file

@ -31,8 +31,8 @@ NewForm Component
const NewForm = ({ closeModal, successCallback, ...props }) => {
const success = successCallback
? () => {
successCallback();
? document => {
successCallback(document);
closeModal();
}
: closeModal;

View file

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

View file

@ -12,18 +12,18 @@ const wrapper = {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}
};
const header = {
textAlign: 'center',
}
};
const code = {
border: '1px solid #ccc',
borderRadius: 3,
padding: '10px 20px',
background: 'white',
}
};
const Welcome = props =>
<div style={wrapper}>
@ -47,7 +47,7 @@ addRoute({ name: 'home', path: '/', componentName: 'HelloWorld' });
</div>
</div>
</div>;
Welcome.displayName = 'Welcome';

View file

@ -32,5 +32,5 @@ export default function withAccess (options) {
AccessComponent.displayName = `withAccess(${WrappedComponent.displayName})`;
return withRouter(withCurrentUser(AccessComponent));
}
};
}

View file

@ -0,0 +1,28 @@
/**
* This HOC will load the global Components.
* If a "components" prop is passed, it will be merged with the global Components.
*
* This allow local replacement of global components, for example if
* you want a specific submit button but only for one specific form.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { mergeWithComponents } from 'meteor/vulcan:lib';
const withComponents = C => {
const WrappedComponent = ({ components, formComponents, ...otherProps }) => {
//if (formComponents){
// console.warn('"formComponents" prop is deprecated, use "components" prop instead (same behaviour)');
//}
const Components = mergeWithComponents(components || formComponents);
return <C Components={Components} {...otherProps} />;
};
WrappedComponent.displayName = `withComponents(${C.displayName})`;
WrappedComponent.propTypes = {
formComponents: PropTypes.object,
components: PropTypes.object
};
return WrappedComponent;
};
export default withComponents;

View file

@ -30,7 +30,7 @@ import React, { Component } from 'react';
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
import { createClientTemplate } from 'meteor/vulcan:core';
import { extractCollectionInfo, extractFragmentInfo } from './handleOptions';
import { extractCollectionInfo, extractFragmentInfo } from 'meteor/vulcan:lib';
const withCreate = options => {
const { collectionName, collection } = extractCollectionInfo(options);

View file

@ -26,6 +26,6 @@ const withCurrentUser = component => {
},
}
)(component);
}
};
export default withCurrentUser;

View file

@ -30,7 +30,7 @@ import React, { Component } from 'react';
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
import { deleteClientTemplate } from 'meteor/vulcan:core';
import { extractCollectionInfo, extractFragmentInfo } from './handleOptions';
import { extractCollectionInfo, extractFragmentInfo } from 'meteor/vulcan:lib';
const withDelete = options => {
const { collectionName, collection } = extractCollectionInfo(options);

View file

@ -36,16 +36,16 @@ Terms object can have the following properties:
import { withApollo, graphql } from 'react-apollo';
import gql from 'graphql-tag';
import { getSetting, Utils, multiClientTemplate } from 'meteor/vulcan:lib';
import update from 'immutability-helper';
import { getSetting, Utils, multiClientTemplate, extractCollectionInfo, extractFragmentInfo } from 'meteor/vulcan:lib';
import Mingo from 'mingo';
import compose from 'recompose/compose';
import withState from 'recompose/withState';
import { extractCollectionInfo, extractFragmentInfo } from './handleOptions';
export default function withMulti(options) {
// console.log(options)
const {
let {
limit = 10,
pollInterval = getSetting('pollInterval', 20000),
enableTotal = true,
@ -53,6 +53,10 @@ export default function withMulti(options) {
extraQueries
} = options;
// if this is the SSR process, set pollInterval to null
// see https://github.com/apollographql/apollo-client/issues/1704#issuecomment-322995855
pollInterval = typeof window === 'undefined' ? null : pollInterval;
const { collectionName, collection } = extractCollectionInfo(options);
const { fragmentName, fragment } = extractFragmentInfo(options, collectionName);
@ -156,8 +160,8 @@ export default function withMulti(options) {
typeof providedTerms === 'undefined'
? {
/*...props.ownProps.terms,*/ ...props.ownProps.paginationTerms,
limit: results.length + props.ownProps.paginationTerms.itemsPerPage
}
limit: results.length + props.ownProps.paginationTerms.itemsPerPage
}
: providedTerms;
props.ownProps.setPaginationTerms(newTerms);

View file

@ -23,7 +23,7 @@ export default function withMutation({name, args, fragmentName}) {
fragment = getFragment(fragmentName);
fragmentBlock = `{
...${fragmentName}
}`
}`;
}
if (args) {
@ -33,13 +33,13 @@ export default function withMutation({name, args, fragmentName}) {
mutation ${name}(${args1}) {
${name}(${args2})${fragmentBlock}
}
`
`;
} else {
mutation = `
mutation ${name} {
${name}${fragmentBlock}
}
`
`;
}
return graphql(gql`${mutation}${fragmentName ? fragment : ''}`, {

View file

@ -2,12 +2,15 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
import { getSetting, singleClientTemplate, Utils } from 'meteor/vulcan:lib';
import { extractCollectionInfo, extractFragmentInfo } from './handleOptions';
import { getSetting, singleClientTemplate, Utils, extractCollectionInfo, extractFragmentInfo } from 'meteor/vulcan:lib';
export default function withSingle(options) {
const { pollInterval = getSetting('pollInterval', 20000), enableCache = false, extraQueries } = options;
let { pollInterval = getSetting('pollInterval', 20000), enableCache = false, extraQueries } = options;
// if this is the SSR process, set pollInterval to null
// see https://github.com/apollographql/apollo-client/issues/1704#issuecomment-322995855
pollInterval = typeof window === 'undefined' ? null : pollInterval;
const { collectionName, collection } = extractCollectionInfo(options);
const { fragmentName, fragment } = extractFragmentInfo(options, collectionName);
@ -46,6 +49,7 @@ export default function withSingle(options) {
const propertyName = options.propertyName || 'document';
const props = {
loading: data.loading,
refetch: data.refetch,
// document: Utils.convertDates(collection, data[singleResolverName]),
[propertyName]: data[resolverName] && data[resolverName].result,
fragmentName,

View file

@ -0,0 +1,32 @@
import React, { Component } from 'react';
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
const withSiteData = component => {
return graphql(
gql`
query getSiteData {
SiteData {
url
title
sourceVersion
logoUrl
}
}
`, {
alias: 'withSiteData',
props(props) {
const { data } = props;
return {
siteDataLoading: data.loading,
siteData: data.SiteData,
siteDataData: data,
};
},
}
)(component);
};
export default withSiteData;

View file

@ -30,11 +30,9 @@ Child Props:
import React, { Component } from 'react';
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
import { updateClientTemplate } from 'meteor/vulcan:lib';
import { updateClientTemplate, extractCollectionInfo, extractFragmentInfo } from 'meteor/vulcan:lib';
import clone from 'lodash/clone';
import { extractCollectionInfo, extractFragmentInfo } from './handleOptions';
const withUpdate = options => {
const { collectionName, collection } = extractCollectionInfo(options);
const { fragmentName, fragment } = extractFragmentInfo(options, collectionName);

View file

@ -33,7 +33,7 @@ import gql from 'graphql-tag';
import { upsertClientTemplate } from 'meteor/vulcan:core';
import clone from 'lodash/clone';
import { extractCollectionInfo, extractFragmentInfo } from './handleOptions';
import { extractCollectionInfo, extractFragmentInfo } from 'meteor/vulcan:lib';
const withUpsert = options => {
const { collectionName, collection } = extractCollectionInfo(options);

View file

@ -73,7 +73,7 @@ export function getDefaultMutations(options) {
const collection = context[collectionName];
// check if current user can pass check function; else throw error
Utils.performCheck(this.check, context.currentUser, data);
Utils.performCheck(this.check, context.currentUser, data, '', `${typeName}.create`, collectionName);
// pass document to boilerplate newMutator function
return await createMutator({
@ -167,7 +167,7 @@ export function getDefaultMutations(options) {
}
// check if user can perform operation; if not throw error
Utils.performCheck(this.check, context.currentUser, document);
Utils.performCheck(this.check, context.currentUser, document, document._id, `${typeName}.update`, collectionName);
// call editMutator boilerplate function
return await updateMutator({
@ -282,7 +282,7 @@ export function getDefaultMutations(options) {
throw new Error(`Could not find document to delete for selector: ${JSON.stringify(selector)}`);
}
Utils.performCheck(this.check, context.currentUser, document, context);
Utils.performCheck(this.check, context.currentUser, document, context, document._id, `${typeName}.delete`, collectionName);
return await deleteMutator({
collection,

View file

@ -4,8 +4,7 @@ Default list, single, and total resolvers
*/
import { Utils, debug, debugGroup, debugGroupEnd, Connectors, getTypeName, getCollectionName } from 'meteor/vulcan:lib';
import { createError } from 'apollo-errors';
import { Utils, debug, debugGroup, debugGroupEnd, Connectors, getTypeName, getCollectionName, throwError } from 'meteor/vulcan:lib';
const defaultOptions = {
cacheMaxAge: 300
@ -117,15 +116,22 @@ export function getDefaultResolvers(options) {
if (allowNull) {
return { result: null };
} else {
const MissingDocumentError = createError('app.missing_document', { message: 'app.missing_document' });
throw new MissingDocumentError({ data: { documentId, selector } });
throwError({ id: 'app.missing_document', data: {documentId, selector} });
}
}
// if collection has a checkAccess function defined, use it to perform a check on the current document
// (will throw an error if check doesn't pass)
if (collection.checkAccess) {
Utils.performCheck(collection.checkAccess, currentUser, doc, collection, documentId);
Utils.performCheck(
collection.checkAccess,
currentUser,
doc,
collection,
documentId,
`${typeName}.read.single`,
collectionName
);
}
const restrictedDoc = Users.restrictViewableFields(currentUser, collection, doc);

View file

@ -36,6 +36,7 @@ export { default as withCurrentUser } from './containers/withCurrentUser.js';
export { default as withMutation } from './containers/withMutation.js';
export { default as withUpsert } from './containers/withUpsert.js';
export { default as withComponents } from './containers/withComponents';
// OpenCRUD backwards compatibility
export { default as withNew } from './containers/withCreate.js';

View file

@ -1,7 +1,7 @@
Package.describe({
name: 'vulcan:core',
summary: 'Vulcan core package',
version: '1.12.8',
version: '1.12.13',
git: 'https://github.com/VulcanJS/Vulcan.git'
});
@ -9,19 +9,20 @@ Package.onUse(function (api) {
api.versionsFrom('1.6.1');
api.use([
'vulcan:lib@1.12.8',
'vulcan:i18n@1.12.8',
'vulcan:users@1.12.8',
'vulcan:debug@1.12.8'
'vulcan:lib@1.12.13',
'vulcan:i18n@1.12.13',
'vulcan:users@1.12.13',
'vulcan:routing@1.12.13',
'vulcan:debug@1.12.13'
]);
api.imply(['vulcan:lib@1.12.8']);
api.imply(['vulcan:lib@1.12.13']);
api.mainModule('lib/server/main.js', 'server');
api.mainModule('lib/client/main.js', 'client');
});
Package.onTest(function (api) {
api.use(['ecmascript', 'meteortesting:mocha', 'vulcan:core']);
api.use(['ecmascript', 'meteortesting:mocha', 'vulcan:test', 'vulcan:core']);
api.mainModule('./test/index.js');
});

View file

@ -0,0 +1,74 @@
// setup JSDOM server side for testing (necessary for Enzyme to mount)
import 'jsdom-global/register';
import React from 'react';
import expect from 'expect';
import { mount, shallow } from 'enzyme';
import { Components } from 'meteor/vulcan:core';
import { initComponentTest } from 'meteor/vulcan:test';
// we must import all the other components, so that "registerComponent" is called
import '../lib/modules';
import Datatable from '../lib/modules/components/Datatable';
// stub collection
import { createCollection, getDefaultResolvers, getDefaultMutations, registerFragment } from 'meteor/vulcan:core';
const createDummyCollection = (typeName, schema) => {
return createCollection({
collectionName: typeName + 's',
typeName,
schema,
resolvers: getDefaultResolvers(typeName + 's'),
mutations: getDefaultMutations(typeName + 's')
});
};
const Articles = createDummyCollection('Article', {
name: {
type: String
}
});
registerFragment(`
fragment ArticlesDefaultFragment on Article {
name
}
`);
// setup Vulcan (load components, initialize fragments)
initComponentTest();
describe('vulcan-core/components', function () {
describe('DataTable', function () {
it('shallow renders DataTable', function () {
const wrapper = shallow(<Datatable
Components={Components}
collection={Articles} />);
expect(wrapper).toBeDefined();
});
it('render a static version', function () {
const wrapper = shallow(<Datatable
Components={Components}
data={[{ name: 'foo' }, { name: 'bar' }]} />);
const content = wrapper.find('DatatableContents').first();
expect(content).toBeDefined();
});
const context = {
intl: {
formatMessage: () => { },
}
};
it('mounts a static version', function () {
const wrapper = mount(
<Datatable
Components={Components}
data={[{ name: 'foo' }, { name: 'bar' }]}
/>
, {
context,
childContextTypes: context
});
expect(wrapper).toBeDefined();
//const content = wrapper.find('DatatableContents').first();
//expect(content).toBeDefined();
});
});
});

View file

@ -1,18 +1,45 @@
import { extractCollectionInfo, extractFragmentInfo } from '../lib/modules/containers/handleOptions';
// setup JSDOM server side for testing (necessary for Enzyme to mount)
import 'jsdom-global/register';
import React from 'react';
import expect from 'expect';
import { shallow } from 'enzyme';
import { Components } from 'meteor/vulcan:core';
import { initComponentTest } from 'meteor/vulcan:test';
import { withComponents } from '../lib/modules';
describe('vulcan-core/containers', function() {
describe('handleOptions', function() {
// we must import all the other components, so that "registerComponent" is called
import '../lib/modules';
// setup Vulcan (load components, initialize fragments)
initComponentTest();
describe('vulcan-core/containers', function () {
describe('withComponents', function () {
it('should override components', function () {
// replace any component for testing purpose
const firstComponentName = Components[Object.keys(Components)[0]];
const FooComponent = () => 'FOO';
const components = { [firstComponentName]: FooComponent };
const MyComponent = withComponents(({ Components }) => Components[firstComponentName]());
const wrapper = shallow(<MyComponent components={components} />);
expect(wrapper.prop('Components')).toBeDefined();
expect(wrapper.prop('Components')[firstComponentName]).toEqual(FooComponent);
expect(wrapper.html()).toEqual('FOO');
});
});
describe('handleOptions', function () {
const expectedCollectionName = 'COLLECTION_NAME';
const collectionNameOptions = { collectionName: expectedCollectionName };
const expectedCollection = { options: collectionNameOptions };
it('get collectionName from collection', function() {
it('get collectionName from collection', function () {
const options = { collection: expectedCollection };
const { collection, collectionName } = extractCollectionInfo(options);
expect(collection).toEqual(expectedCollection);
expect(collectionName).toEqual(expectedCollectionName);
});
it('get collection from collectioName', function() {
it('get collection from collectioName', function () {
// MOCK getCollection
const { collection, collectionName } = extractCollectionInfo(collectionNameOptions);
expect(collection).toEqual(expectedCollection);
@ -20,20 +47,20 @@ describe('vulcan-core/containers', function() {
});
const expectedFragmentName = 'FRAGMENT_NAME';
const expectedFragment = { definitions: [{ name: { value: expectedFragmentName } }] };
it('get fragment from fragmentName', function() {
it('get fragment from fragmentName', function () {
// MOCK getCollection
const options = { fragmentName: expectedFragmentName };
const { fragment, fragmentName } = extractFragmentInfo(options);
expect(fragment).toEqual(expectedFragment);
expect(fragmentName).toEqual(expectedFragmentName);
});
it('get fragmentName from fragment', function() {
it('get fragmentName from fragment', function () {
const options = { fragment: expectedFragment };
const { fragment, fragmentName } = extractFragmentInfo(options);
expect(fragment).toEqual(expectedFragment);
expect(fragmentName).toEqual(expectedFragmentName);
});
it('get fragmentName and fragment from collectionName', function() {
it('get fragmentName and fragment from collectionName', function () {
// if options does not contain fragment, we get the collection default fragment based on its name
const options = {};
const { fragment, fragmentName } = extractFragmentInfo(options, expectedCollectionName);
@ -42,13 +69,13 @@ describe('vulcan-core/containers', function() {
});
});
describe('withMessages', function() {
describe('withMessages', function () {
const WrappedComponent = props => <div />;
const apolloClient = null; // TODO: init an apolloClient, that must be available in the context
it.skip('pass messages', function() {});
it.skip('add a flash message', function() {});
it.skip('mark a flash message as seen', function() {});
it.skip('hide a flash message as seen', function() {});
it.skip('clear seen', function() {});
it.skip('pass messages', function () { });
it.skip('add a flash message', function () { });
it.skip('mark a flash message as seen', function () { });
it.skip('hide a flash message as seen', function () { });
it.skip('clear seen', function () { });
});
});

View file

@ -1 +1,3 @@
import './containers.test.js';
import './resolvers.test';
import './components.test';
import './containers.test';

View file

@ -0,0 +1,84 @@
import expect from 'expect';
import { getDefaultResolvers } from '../lib/modules/default_resolvers';
describe('vulcan:core/default_resolvers', function() {
const resolversOptions = {
typeName: 'Dummy',
collectionName: 'Dummies',
options: {}
};
describe('single', function() {
it('defines the correct fields', function() {
const { single } = getDefaultResolvers(resolversOptions);
const { description, resolver } = single;
expect(description).toBeDefined();
expect(resolver).toBeDefined();
expect(resolver).toBeInstanceOf(Function);
});
const buildContext = ({ load = () => null, currentUser = null }) => ({
Dummies: {
options: { collectionName: 'Dummies' },
loader: { load }
//findOne() {
// console.log('FINDE_ONE');
//}
}, //TODO fake collection
Users: {
restrictViewableFields: (currentUser, collection, doc) => doc
},
currentUser
});
// TODO: what's the name of this argument? handles cache
const lastArg = { cacheControl: {} };
// eslint-disable-next-line no-unused-vars
const loggedInUser = { _id: 'foobar', groups: [], isAdmin: false };
// eslint-disable-next-line no-unused-vars
const adminUser = { _id: 'foobar', groups: [], isAdmin: true };
const getSingleResolver = () => getDefaultResolvers(resolversOptions).single.resolver;
// TODO: the current behaviour is not consistent, could be improved
// @see https://github.com/VulcanJS/Vulcan/issues/2118
it.skip('return null if documentId is undefined', function() {
const resolver = getSingleResolver();
// no documentId
const input = { selector: {} };
// non empty db
const context = buildContext({ load: () => ({ _id: 'my-document' }) });
const res = resolver(null, { input }, context, lastArg);
return expect(res).resolves.toEqual({ result: null });
});
it('return document in case of success', function() {
const resolver = getSingleResolver();
const documentId = 'my-document';
const document = { _id: documentId };
const input = { selector: { documentId } };
// non empty db
const context = buildContext({
load: () => {
return document;
}
});
const res = resolver(null, { input }, context, lastArg);
return expect(res).resolves.toEqual({ result: document });
});
it('return null if failure to find doc but allowNull is true', function() {
const resolver = getSingleResolver();
const documentId = 'bad-document';
const input = { selector: { documentId }, allowNull: true };
// empty db
const context = buildContext({ load: () => null });
const res = resolver(null, { input }, context, lastArg);
return expect(res).resolves.toEqual({ result: null });
});
it('throws if documentId is defined but does not match any document', function() {
const resolver = getSingleResolver();
const documentId = 'bad-document';
// eslint-disable-next-line no-unused-vars
const document = { _id: documentId };
const input = { selector: { documentId } };
// empty db
const context = buildContext({ load: () => null });
return expect(resolver(null, { input }, context, lastArg)).rejects.toThrow();
});
});
});

View file

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

View file

@ -3,7 +3,7 @@ import { registerComponent, Components } from 'meteor/vulcan:lib';
import Callbacks from '../modules/callbacks/collection.js';
const CallbacksName = ({ document }) =>
<strong>{document.name}</strong>
<strong>{document.name}</strong>;
const CallbacksDashboard = props =>
<div className="settings">
@ -23,7 +23,7 @@ const CallbacksDashboard = props =>
'hooks',
]}
/>
</div>
</div>;
registerComponent('Callbacks', CallbacksDashboard);

View file

@ -0,0 +1,36 @@
import React from 'react';
import { registerComponent } from 'meteor/vulcan:lib';
import { Link } from 'react-router';
function Dashboard() {
return (
<div>
<h3>Debug Dashboard</h3>
<ul>
<li key="Callbacks">
<Link to="/debug/callbacks">Callbacks</Link>
</li>
<li key="Components">
<Link to="/debug/components">Components</Link>
</li>
<li key="Emails">
<Link to="/debug/emails">Emails</Link>
</li>
<li key="Groups">
<Link to="/debug/groups">Groups</Link>
</li>
<li key="I18n">
<Link to="/debug/i18n">I18n</Link>
</li>
<li key="Routes">
<Link to="/debug/routes">Routes</Link>
</li>
<li key="Settings">
<Link to="/debug/settings">Settings</Link>
</li>
</ul>
</div>
);
}
registerComponent({ name: 'DebugDashboard', component: Dashboard, hocs: [] });

View file

@ -10,7 +10,7 @@ class Email extends PureComponent {
this.sendTest = this.sendTest.bind(this);
this.state = {
loading: false
}
};
}
sendTest() {
@ -44,7 +44,7 @@ class Email extends PureComponent {
</div>
</td>
</tr>
)
);
}
}
@ -81,7 +81,7 @@ const Emails = (/* props*/) => {
</div>
</div>
)
);
};
registerComponent('Emails', Emails);

View file

@ -8,8 +8,8 @@ const Group = ({name, actions}) => {
<td>{name}</td>
<td><ul>{actions.map((action, index) => <li key={index}><code>{action}</code></li>)}</ul></td>
</tr>
)
}
);
};
const Groups = props => {
return (
@ -33,8 +33,8 @@ const Groups = props => {
</div>
</div>
)
}
);
};
registerComponent('Groups', Groups);

View file

@ -0,0 +1,70 @@
import React from 'react';
import { registerComponent, Components, Strings, Locales } from 'meteor/vulcan:lib';
import PropTypes from 'prop-types';
import sortedUniq from 'lodash/sortedUniq';
/**
* Internationalization debugging page
*
*
**/
function LocaleSwitcher(props, context) {
return (
<div>
<span>Switch locales :</span>
{Locales.map(localeObj => (
<Components.Button key={localeObj.id} onClick={() => context.setLocale(localeObj.id)}>
{localeObj.label}
</Components.Button>
))}
</div>
);
}
LocaleSwitcher.contextTypes = {
getLocale: PropTypes.func,
setLocale: PropTypes.func,
};
export const I18n = (props, context) => {
// translations holds all the translations ids
let translations = [];
let columns = [
{
name: 'id',
component: function({ document }) {
return document;
},
},
];
// reunite all the ids in a single array (translations) and create the columns for each language
Object.keys(Strings).forEach(language => {
translations.push(...Object.keys(Strings[language]));
columns.push({
name: language,
component: function({ document }) {
return Strings[language][document] || null;
},
});
});
//sort the array
translations.sort();
//remove duplicates
let translationsUniq = sortedUniq(translations);
return (
<div>
<h3>{'Your current locale: ' + context.getLocale()}</h3>
<LocaleSwitcher />
<Components.Datatable showSearch={false} showNew={false} showEdit={false} data={translationsUniq} columns={columns} />
</div>
);
};
I18n.contextTypes = {
getLocale: PropTypes.func,
setLocale: PropTypes.func,
};
registerComponent({ name: 'I18n', component: I18n, hocs: [] });

View file

@ -3,7 +3,7 @@ import { registerComponent, Components, Routes } from 'meteor/vulcan:lib';
import { Link } from 'react-router-dom';
const RoutePath = ({ document }) =>
<Link to={document.path}>{document.path}</Link>
<Link to={document.path}>{document.path}</Link>;
const RoutesDashboard = props =>
<div className="routes">
@ -21,6 +21,6 @@ const RoutesDashboard = props =>
'componentName',
]}
/>
</div>
</div>;
registerComponent('Routes', RoutesDashboard);

View file

@ -3,7 +3,7 @@ import { registerComponent, Components } from 'meteor/vulcan:lib';
import Settings from '../modules/settings/collection.js';
const SettingName = ({ document }) =>
<strong>{document.name}</strong>
<strong>{document.name}</strong>;
const SettingsDashboard = props =>
<div className="settings">
@ -20,7 +20,7 @@ const SettingsDashboard = props =>
'serverOnly'
]}
/>
</div>
</div>;
registerComponent('Settings', SettingsDashboard);

View file

@ -6,3 +6,5 @@ import '../components/Settings.jsx';
import '../components/Callbacks.jsx';
import '../components/Routes.jsx';
import '../components/Components.jsx';
import '../components/I18n.jsx';
import '../components/Dashboard.jsx';

View file

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

View file

@ -1,7 +1,7 @@
Package.describe({
name: 'vulcan:debug',
summary: 'Vulcan debug package',
version: '1.12.8',
version: '1.12.13',
git: 'https://github.com/VulcanJS/Vulcan.git',
debugOnly: true
});
@ -12,13 +12,13 @@ Package.onUse(function (api) {
api.use([
'fourseven:scss@4.5.0',
'fourseven:scss@4.10.0',
'dynamic-import@0.1.1',
// Vulcan packages
'vulcan:lib@1.12.8',
'vulcan:email@1.12.8',
'vulcan:lib@1.12.13',
'vulcan:email@1.12.13',
]);

View file

@ -3,7 +3,27 @@ import VulcanEmail from '../namespace.js';
import Juice from 'juice';
import htmlToText from 'html-to-text';
import Handlebars from 'handlebars';
import { Utils, getSetting, registerSetting, runQuery, Strings } from 'meteor/vulcan:lib'; // import from vulcan:lib because vulcan:core is not loaded yet
import { Utils, getSetting, registerSetting, runQuery, Strings, getString } from 'meteor/vulcan:lib'; // import from vulcan:lib because vulcan:core is not loaded yet
/*
Get intl string. Usage: {{__ "posts.create"}}
*/
Handlebars.registerHelper('__', function(id, context) {
const s = getString({ id, locale: context.data.root.locale });
return new Handlebars.SafeString(s);
});
/*
Get intl string, accepts a second variables argument. Usage: {{__ "posts.create" postVariables}}
*/
Handlebars.registerHelper('___', function(id, variables, context) {
const s = getString({ id, variables, locale: context.data.root.locale });
return new Handlebars.SafeString(s);
});
registerSetting('secondaryColor', '#444444');
registerSetting('accentColor', '#DD3416');
@ -23,8 +43,12 @@ VulcanEmail.addTemplates = templates => {
_.extend(VulcanEmail.templates, templates);
};
VulcanEmail.getTemplate = templateName =>
Handlebars.compile(VulcanEmail.templates[templateName], { noEscape: true, strict: true });
VulcanEmail.getTemplate = templateName => {
if (!VulcanEmail.templates[templateName]) {
throw new Error(`Couldn't find email template named “${templateName}`);
}
return Handlebars.compile(VulcanEmail.templates[templateName], { noEscape: true, strict: true });
};
VulcanEmail.buildTemplate = (htmlContent, data = {}, locale) => {
const emailProperties = {
@ -58,18 +82,18 @@ VulcanEmail.generateTextVersion = html => {
});
};
VulcanEmail.send = (to, subject, html, text, throwErrors, cc, bcc, replyTo) => {
VulcanEmail.send = (to, subject, html, text, throwErrors, cc, bcc, replyTo, headers) => {
// TODO: limit who can send emails
// TODO: fix this error: Error: getaddrinfo ENOTFOUND
if (typeof to === 'object') {
// eslint-disable-next-line no-redeclare
var { to, cc, bcc, replyTo, subject, html, text, throwErrors } = to;
var { to, cc, bcc, replyTo, subject, html, text, throwErrors, headers } = to;
}
const from = getSetting('defaultEmail', 'noreply@example.com');
const siteName = getSetting('title', 'Vulcan');
subject = '[' + siteName + '] ' + subject;
subject = subject || '[' + siteName + ']';
if (typeof text === 'undefined') {
// Auto-generate text version if it doesn't exist. Has bugs, but should be good enough.
@ -83,19 +107,22 @@ VulcanEmail.send = (to, subject, html, text, throwErrors, cc, bcc, replyTo) => {
bcc: bcc,
replyTo: replyTo,
subject: subject,
headers: headers,
text: text,
html: html,
};
if (process.env.NODE_ENV === 'production' || getSetting('enableDevelopmentEmails', false)) {
console.log('//////// sending email…'); // eslint-disable-line
console.log('from: ' + from); // eslint-disable-line
console.log('cc: ' + cc); // eslint-disable-line
console.log('bcc: ' + bcc); // eslint-disable-line
console.log('replyTo: ' + replyTo); // eslint-disable-line
// console.log('html: '+html);
// console.log('text: '+text);
const shouldSendEmail = process.env.NODE_ENV === 'production' || getSetting('enableDevelopmentEmails', false);
console.log(`//////// sending email${shouldSendEmail ? '' : ' (simulation)'}`); // eslint-disable-line
console.log('from: ' + from); // eslint-disable-line
console.log('to: ' + to); // eslint-disable-line
console.log('cc: ' + cc); // eslint-disable-line
console.log('bcc: ' + bcc); // eslint-disable-line
console.log('replyTo: ' + replyTo); // eslint-disable-line
console.log('headers: ' + JSON.stringify(headers)); // eslint-disable-line
if (shouldSendEmail) {
try {
Email.send(email);
} catch (error) {
@ -103,13 +130,6 @@ VulcanEmail.send = (to, subject, html, text, throwErrors, cc, bcc, replyTo) => {
console.log(error); // eslint-disable-line
if (throwErrors) throw error;
}
} else {
console.log('//////// sending email (simulation)…'); // eslint-disable-line
console.log('from: ' + from); // eslint-disable-line
console.log('to: ' + to); // eslint-disable-line
console.log('cc: ' + cc); // eslint-disable-line
console.log('bcc: ' + bcc); // eslint-disable-line
console.log('replyTo: ' + replyTo); // eslint-disable-line
}
return email;
@ -121,20 +141,21 @@ VulcanEmail.build = async ({ emailName, variables, locale }) => {
const result = email.query ? await runQuery(email.query, variables, { locale }) : { data: {} };
// if email has a data() function, merge its return value with results from the query
const data = email.data ? { ...result.data, ...email.data(variables) } : result.data;
const data = email.data ? { ...result.data, ...email.data({ data: result.data, variables, locale }) } : result.data;
const subject = typeof email.subject === 'function' ? email.subject(data) : email.subject;
const subject = typeof email.subject === 'function' ? email.subject({ data, variables, locale }) : email.subject;
data.__ = Strings[locale];
data.locale = locale;
const html = VulcanEmail.buildTemplate(VulcanEmail.getTemplate(email.template)(data), data, locale);
return { data, subject, html };
};
VulcanEmail.buildAndSend = async ({ to, cc, bcc, replyTo, emailName, variables, locale = getSetting('locale') }) => {
VulcanEmail.buildAndSend = async ({ to, cc, bcc, replyTo, emailName, variables, locale = getSetting('locale'), headers }) => {
const email = await VulcanEmail.build({ to, emailName, variables, locale });
return VulcanEmail.send({ to, cc, bcc, replyTo, subject: email.subject, html: email.html });
return VulcanEmail.send({ to, cc, bcc, replyTo, subject: email.subject, html: email.html, headers });
};
VulcanEmail.buildAndSendHTML = (to, subject, html) => VulcanEmail.send(to, subject, VulcanEmail.buildTemplate(html));

View file

@ -15,7 +15,7 @@ Meteor.startup(function() {
// else get test object (sample post, comment, user, etc.)
const testVariables =
(typeof email.testVariables === 'function' ? email.testVariables() : email.testVariables) || {};
(typeof email.testVariables === 'function' ? email.testVariables(params) : email.testVariables) || {};
// delete params.query so we don't pass it to GraphQL query
delete params.query;
// merge test variables with params from URL
@ -33,6 +33,11 @@ Meteor.startup(function() {
html = `
${builtHtml}
<h4 style="margin: 20px;"><code>Subject: ${subject}</code></h4>
<h5 style="margin: 20px;">Variables:</h5>
<div style="border: 1px solid #999; padding: 10px 20px; margin: 20px;">
<pre><code>${JSON.stringify(variables, null, 2)}</code></pre>
</div>
<h5 style="margin: 20px;">Data:</h5>
<div style="border: 1px solid #999; padding: 10px 20px; margin: 20px;">
<pre><code>${JSON.stringify(emailTestData, null, 2)}</code></pre>
</div>

View file

@ -1,7 +1,7 @@
Package.describe({
name: 'vulcan:email',
summary: 'Vulcan email package',
version: '1.12.8',
version: '1.12.13',
git: 'https://github.com/VulcanJS/Vulcan.git'
});
@ -10,7 +10,7 @@ Package.onUse(function (api) {
api.versionsFrom('1.6.1');
api.use([
'vulcan:lib@1.12.8'
'vulcan:lib@1.12.13'
]);
api.mainModule('lib/server.js', 'server');

View file

@ -27,10 +27,10 @@ Embed.builtin = {
title: metadata.title,
description: metadata.description,
thumbnailUrl: metadata.image,
}
};
}
}
};
// -------------- //
// adapted from https://github.com/acemtp/meteor-meta-extractor/blob/master/meta-extractor.js

View file

@ -40,7 +40,7 @@ if (settings) {
const embedData = {
title: data.title,
description: data.description
}
};
if (data.pics && data.pics.length > 0) {
embedData.thumbnailUrl = data.pics[0];
@ -63,7 +63,7 @@ if (settings) {
}
},
}
};
}

View file

@ -38,7 +38,7 @@ if (settings) {
if (data.images && data.images.length > 0) // there may not always be an image
data.thumbnailUrl = data.images[0].url.replace('http:','') // add thumbnailUrl as its own property
data.thumbnailUrl = data.images[0].url.replace('http:',''); // add thumbnailUrl as its own property
if (data.authors && data.authors.length > 0) {
data.sourceName = data.authors[0].name;
@ -60,7 +60,7 @@ if (settings) {
}
},
}
};
}

View file

@ -1,7 +1,7 @@
export * from '../modules/index.js';
import './integrations/builtin.js'
import './integrations/embedly.js'
import './integrations/embedapi.js'
import './integrations/builtin.js';
import './integrations/embedly.js';
import './integrations/embedapi.js';
import './mutations.js';

View file

@ -1,7 +1,7 @@
Package.describe({
name: 'vulcan:embed',
summary: 'Vulcan Embed package',
version: '1.12.8',
version: '1.12.13',
git: 'https://github.com/VulcanJS/Vulcan.git'
});
@ -11,8 +11,8 @@ Package.onUse( function(api) {
api.use([
'http',
'vulcan:core@1.12.8',
'fourseven:scss@4.5.0'
'vulcan:core@1.12.13',
'fourseven:scss@4.10.0'
]);

View file

@ -0,0 +1 @@
Vulcan error tracking adapter for Sentry.

View file

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

View file

@ -0,0 +1,52 @@
import { getSetting } from 'meteor/vulcan:core';
import { addInitFunction, addLogFunction, addUserFunction } from 'meteor/vulcan:errors';
import Sentry from '@sentry/browser';
import { clientDSNSetting } from '../modules/settings';
import { getUserObject } from '../modules/sentry';
const clientDSN = getSetting(clientDSNSetting);
const environment = getSetting('environment');
/*
Initialize Sentry
*/
function initSentryForClient(props = {}) {
Sentry.init({
dsn: clientDSN,
environment,
release: props.siteData && props.siteData.sourceVersion
});
}
addInitFunction(initSentryForClient);
/*
Log an error, and optionally set current user as well
*/
function logToSentry({ error, details, currentUser }) {
Sentry.withScope(scope => {
if (currentUser) {
scope.setUser(getUserObject(currentUser));
}
Object.keys(details).forEach(key => {
scope.setExtra(key, details[key]);
});
Sentry.captureException(error);
});
}
addLogFunction(logToSentry);
/*
Set the current user
*/
function setSentryUser(currentUser) {
Sentry.configureScope(scope => {
scope.setUser(getUserObject(currentUser));
});
}
addUserFunction(setSentryUser);

View file

@ -0,0 +1,2 @@
export * from './settings';
// import './logToRollbar';

View file

@ -0,0 +1,5 @@
export const getUserObject = currentUser => ({
id: currentUser._id,
username: currentUser.displayName,
email: currentUser.email,
});

View file

@ -0,0 +1,8 @@
import { registerSetting } from 'meteor/vulcan:core';
export const clientDSNSetting = 'sentry.clientDSN';
export const serverDSNSetting = 'sentry.serverDSN';
export const tokensUrl = 'https://sentry.io/onboarding/{account}/{project}/configure/node';
registerSetting(clientDSNSetting, null, `Sentry client DSN access token (from ${tokensUrl})`);
registerSetting(serverDSNSetting, null, `Sentry client DSN access token (from ${tokensUrl})`);

View file

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

View file

@ -0,0 +1,53 @@
import { getSetting, getSourceVersion } from 'meteor/vulcan:core';
import { addInitFunction, addLogFunction, addUserFunction } from 'meteor/vulcan:errors';
import { serverDSNSetting } from '../modules/settings';
import Sentry from '@sentry/node';
import { getUserObject } from '../modules/sentry';
const serverDSN = getSetting(serverDSNSetting);
const environment = getSetting('environment');
/*
Initialize Sentry
*/
function initSentryForServer() {
Sentry.init({
dsn: serverDSN,
environment,
// see https://github.com/zodern/meteor-up/issues/807#issuecomment-346915622
release: getSourceVersion(),
});
}
addInitFunction(initSentryForServer);
/*
Log an error, and optionally set current user as well
*/
function logToSentry({ error, details, currentUser }) {
Sentry.withScope(scope => {
if (currentUser) {
scope.setUser(getUserObject(currentUser));
}
Object.keys(details).forEach(key => {
scope.setExtra(key, details[key]);
});
Sentry.captureException(error);
});
}
addLogFunction(logToSentry);
/*
Set the current user
*/
function setSentryUser(currentUser) {
Sentry.configureScope(scope => {
scope.setUser(getUserObject(currentUser));
});
}
addUserFunction(setSentryUser);

View file

@ -0,0 +1,22 @@
Package.describe({
name: 'vulcan:errors-sentry',
summary: 'Vulcan Sentry error tracking package',
version: '1.12.13',
git: 'https://github.com/VulcanJS/Vulcan.git'
});
Package.onUse(function(api) {
api.versionsFrom('1.6.1');
api.use([
'ecmascript',
'vulcan:core@1.12.13',
'vulcan:users@1.12.13',
'vulcan:errors@1.12.13',
]);
api.mainModule('lib/server/main.js', 'server');
api.mainModule('lib/client/main.js', 'client');
});

Some files were not shown because too many files have changed in this diff Show more