mirror of
https://github.com/vale981/Vulcan
synced 2025-03-04 09:11:43 -05:00
Merge Apollo2 branch
This commit is contained in:
commit
022a72e41b
258 changed files with 4458 additions and 11839 deletions
|
@ -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
17
.github/stale.yml
vendored
Normal 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
|
|
@ -1 +1 @@
|
|||
METEOR@1.7.0.5
|
||||
METEOR@1.8
|
||||
|
|
|
@ -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
21
.prettierrc.js
Normal 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
76
.vulcan/prettier/index.js
Normal 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);
|
||||
}
|
36
.vulcan/shared/listChangedFiles.js
Normal file
36
.vulcan/shared/listChangedFiles.js
Normal 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;
|
18
.vulcan/shared/pathsByLanguageVersion.js
Normal file
18
.vulcan/shared/pathsByLanguageVersion.js
Normal 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,
|
||||
};
|
12
README.md
12
README.md
|
@ -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
9718
package-lock.json
generated
File diff suppressed because it is too large
Load diff
27
package.json
27
package.json
|
@ -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": {
|
||||
|
|
|
@ -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 => {
|
||||
|
|
12
packages/vulcan-accounts/imports/emailTemplates.js
Normal file
12
packages/vulcan-accounts/imports/emailTemplates.js
Normal 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') +
|
||||
'>';
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ class AccountsEnrollAccount extends PureComponent {
|
|||
|
||||
AccountsEnrollAccount.contextTypes = {
|
||||
intl: intlShape
|
||||
}
|
||||
};
|
||||
|
||||
AccountsEnrollAccount.propsTypes = {
|
||||
currentUser: PropTypes.object,
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
||||
|
|
|
@ -29,7 +29,7 @@ class AccountsResetPassword extends PureComponent {
|
|||
|
||||
AccountsResetPassword.contextTypes = {
|
||||
intl: intlShape
|
||||
}
|
||||
};
|
||||
|
||||
AccountsResetPassword.propsTypes = {
|
||||
currentUser: PropTypes.object,
|
||||
|
|
|
@ -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} />
|
||||
);
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -19,6 +19,6 @@ const AdminHome = ({ currentUser }) =>
|
|||
showEdit={true}
|
||||
/>
|
||||
</Components.ShowIf>
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
export default withCurrentUser(AdminHome);
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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',
|
||||
|
||||
]);
|
||||
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
export * from '../modules/index.js'
|
||||
export * from './make_cloudinary.js'
|
||||
export * from '../modules/index.js';
|
||||
export * from './make_cloudinary.js';
|
||||
|
|
|
@ -2,4 +2,4 @@ import { addCustomFields } from '../modules/index.js';
|
|||
|
||||
export const makeCloudinary = ({collection, fieldName}) => {
|
||||
addCustomFields(collection);
|
||||
}
|
||||
};
|
|
@ -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 => {
|
|||
|
||||
]);
|
||||
|
||||
}
|
||||
};
|
|
@ -1,3 +1,3 @@
|
|||
export * from './cloudinary.js';
|
||||
export * from '../modules/index.js';
|
||||
export * from './make_cloudinary.js'
|
||||
export * from './make_cloudinary.js';
|
||||
|
|
|
@ -39,4 +39,4 @@ export const makeCloudinary = ({collection, fieldName}) => {
|
|||
}
|
||||
addCallback(`${collection.options.collectionName.toLowerCase()}.edit.sync`, cacheImageOnEdit);
|
||||
|
||||
}
|
||||
};
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ const DynamicLoading = ({ isLoading, pastDelay, error }) => {
|
|||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
registerComponent('DynamicLoading', DynamicLoading);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -7,8 +7,8 @@ const Error404 = () => {
|
|||
<div className="error404">
|
||||
<h3><FormattedMessage id="app.404"/></h3>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
Error404.displayName = 'Error404';
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -75,7 +75,7 @@ class HeadTags extends PureComponent {
|
|||
} else {
|
||||
HeadComponent = componentOrArray;
|
||||
}
|
||||
return <HeadComponent key={index} />
|
||||
return <HeadComponent key={index} />;
|
||||
})}
|
||||
|
||||
</div>
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -8,8 +8,8 @@ const Loading = props => {
|
|||
<div className="bounce2"></div>
|
||||
<div className="bounce3"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
Loading.displayName = 'Loading';
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -31,8 +31,8 @@ NewForm Component
|
|||
const NewForm = ({ closeModal, successCallback, ...props }) => {
|
||||
|
||||
const success = successCallback
|
||||
? () => {
|
||||
successCallback();
|
||||
? document => {
|
||||
successCallback(document);
|
||||
closeModal();
|
||||
}
|
||||
: closeModal;
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -32,5 +32,5 @@ export default function withAccess (options) {
|
|||
AccessComponent.displayName = `withAccess(${WrappedComponent.displayName})`;
|
||||
|
||||
return withRouter(withCurrentUser(AccessComponent));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
|
|
|
@ -26,6 +26,6 @@ const withCurrentUser = component => {
|
|||
},
|
||||
}
|
||||
)(component);
|
||||
}
|
||||
};
|
||||
|
||||
export default withCurrentUser;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 : ''}`, {
|
||||
|
|
|
@ -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,
|
||||
|
|
32
packages/vulcan-core/lib/modules/containers/withSiteData.js
Normal file
32
packages/vulcan-core/lib/modules/containers/withSiteData.js
Normal 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;
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
|
74
packages/vulcan-core/test/components.test.js
Normal file
74
packages/vulcan-core/test/components.test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 () { });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
import './containers.test.js';
|
||||
import './resolvers.test';
|
||||
import './components.test';
|
||||
import './containers.test';
|
||||
|
|
84
packages/vulcan-core/test/resolvers.test.js
Normal file
84
packages/vulcan-core/test/resolvers.test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
|
@ -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);
|
||||
|
||||
|
|
36
packages/vulcan-debug/lib/components/Dashboard.jsx
Normal file
36
packages/vulcan-debug/lib/components/Dashboard.jsx
Normal 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: [] });
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
70
packages/vulcan-debug/lib/components/I18n.jsx
Normal file
70
packages/vulcan-debug/lib/components/I18n.jsx
Normal 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: [] });
|
|
@ -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);
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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' },
|
||||
]);
|
||||
|
|
|
@ -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',
|
||||
|
||||
]);
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
|||
}
|
||||
},
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
|||
}
|
||||
},
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
|
@ -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'
|
||||
]);
|
||||
|
||||
|
||||
|
|
1
packages/vulcan-errors-sentry/README.md
Normal file
1
packages/vulcan-errors-sentry/README.md
Normal file
|
@ -0,0 +1 @@
|
|||
Vulcan error tracking adapter for Sentry.
|
2
packages/vulcan-errors-sentry/lib/client/main.js
Normal file
2
packages/vulcan-errors-sentry/lib/client/main.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from '../modules/index';
|
||||
import './sentry-client.js';
|
52
packages/vulcan-errors-sentry/lib/client/sentry-client.js
Normal file
52
packages/vulcan-errors-sentry/lib/client/sentry-client.js
Normal 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);
|
2
packages/vulcan-errors-sentry/lib/modules/index.js
Normal file
2
packages/vulcan-errors-sentry/lib/modules/index.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from './settings';
|
||||
// import './logToRollbar';
|
5
packages/vulcan-errors-sentry/lib/modules/sentry.js
Normal file
5
packages/vulcan-errors-sentry/lib/modules/sentry.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
export const getUserObject = currentUser => ({
|
||||
id: currentUser._id,
|
||||
username: currentUser.displayName,
|
||||
email: currentUser.email,
|
||||
});
|
8
packages/vulcan-errors-sentry/lib/modules/settings.js
Normal file
8
packages/vulcan-errors-sentry/lib/modules/settings.js
Normal 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})`);
|
2
packages/vulcan-errors-sentry/lib/server/main.js
Normal file
2
packages/vulcan-errors-sentry/lib/server/main.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
import './sentry-server.js';
|
||||
export * from '../modules/index';
|
53
packages/vulcan-errors-sentry/lib/server/sentry-server.js
Normal file
53
packages/vulcan-errors-sentry/lib/server/sentry-server.js
Normal 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);
|
22
packages/vulcan-errors-sentry/package.js
Executable file
22
packages/vulcan-errors-sentry/package.js
Executable 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
Loading…
Add table
Reference in a new issue