)
================================================
FILE: .storybook/decorators/MaterialUIDecorator.js
================================================
/*
Use this decorator to load Material UI
*/
import { Components } from 'meteor/vulcan:lib';
// load UI components
import React from 'react'
import 'meteor/vulcan:ui-material/lib/modules/components.js';
import { wrapWithMuiTheme } from 'meteor/vulcan:ui-material';
export default storyFn => (
{storyFn()}
)
================================================
FILE: .storybook/helpers.js
================================================
import merge from 'lodash/merge';
/*
Simplified versions of Vulcan APIs and helpers
*/
/*
Components
*/
export const Components = {}; // will be populated on startup
export const ComponentsMockProps = {};
export const getMockProps = (componentName, overrideProps) => {
return merge({}, ComponentsMockProps[componentName], overrideProps);
};
export function registerComponent(name, rawComponent, ...hocs) {
// support single-argument syntax
if (typeof arguments[0] === 'object') {
// note: cannot use `const` because name, components, hocs are already defined
// as arguments so destructuring cannot work
// eslint-disable-next-line no-redeclare
var { name, component, hocs = [] } = arguments[0];
rawComponent = component;
}
// store the component in the table
Components[name] = rawComponent
}
export const replaceComponent = registerComponent;
export const instantiateComponent = (component, props) => {
if (!component) {
return null;
} else if (typeof component === 'string') {
const Component = getComponent(component);
return ;
} else if (
typeof component === 'function' &&
component.prototype &&
component.prototype.isReactComponent
) {
const Component = component;
return ;
} else if (typeof component === 'function') {
return component(props);
} else {
return component;
}
};
export const coreComponents = [
'Alert',
'Button',
'Dropdown',
'Modal',
'ModalTrigger',
'Table',
'FormComponentCheckbox',
'FormComponentCheckboxGroup',
'FormComponentDate',
'FormComponentDate2',
'FormComponentDateTime',
'FormComponentDefault',
'FormComponentText',
'FormComponentEmail',
'FormComponentNumber',
'FormComponentRadioGroup',
'FormComponentSelect',
'FormComponentSelectMultiple',
'FormComponentStaticText',
'FormComponentTextarea',
'FormComponentTime',
'FormComponentUrl',
'FormControl',
'FormElement',
'FormItem',
];
/*
i18n
*/
export const Strings = {};
export const addStrings = (language, strings) => {
if (typeof Strings[language] === 'undefined') {
Strings[language] = {};
}
Strings[language] = {
...Strings[language],
...strings
};
};
export const Locales = [];
export const registerLocale = locale => {
Locales.push(locale);
};
/*
Users
*/
export const isAdmin = () => true;
export const getProfileUrl = (user, isAbsolute) => {
if (typeof user === 'undefined') {
return '';
}
isAbsolute = typeof isAbsolute === 'undefined' ? false : isAbsolute; // default to false
var prefix = isAbsolute ? Utils.getSiteUrl().slice(0, -1) : '';
if (user.slug) {
return `${prefix}/users/${user.slug}`;
} else {
return '';
}
};
export const getDisplayName = (user) => {
if (!user) {
return '';
} else {
return user.displayName ? user.displayName : Users.getUserName(user);
}
};
export const avatar = {
getUrl: user => 'https://api.adorable.io/avatars/285/abotaat@adorable.io.png',
getInitials: user => 'SG',
}
/*
Helpers
*/
export function capitalize(string) {
return string.replace(/\-/, ' ').split(' ').map(word => {
return word.charAt(0).toUpperCase() + word.slice(1);
}).join(' ');
}
/*
Other Exports
*/
export const getSetting = (name, defaultSetting) => defaultSetting;
export const track = () => {};
export const addCallback = () => {};
export const withCurrentUser = c => c;
export const withUpdate = c => c;
================================================
FILE: .storybook/loaders/starter-example-loader.js
================================================
/**
*
* Load the local Vulcan packages, inspired by vulcan-loader
*
*/
const { getOptions } = require('loader-utils');
module.exports = function loader(source) {
const options = getOptions(this)
const { packagesDir, environment = 'client' } = options
// prefixing your packages name makes it easier to write a loader
const prefix = `${packagesDir}/example-`
const defaultPath = `/lib/${environment}/main.js`
const result = source.replace(
// This regex will match:
// meteor/example-{packageName}{some-optional-import-path}
//
// Example:
// meteor/example-forum => match, packageName="forum"
// meteor/example-forum/foobar.js => match, packageName="forum", importPath="/foobar.js"
// meteor/another-package => do not match
//
// Explanation:
// .+?(?=something) matches every char until "something" is met, excluding something
// we use it to matche the package name, until we meet a ' or "
/meteor\/example-(.*?(?=\/|'|"))(.*?(?=\'|\"))/g, // match Meteor packages that are lfg packages, + the import path (without the quotes)
(match, packageName, importPath) => {
console.log("Found Starter example package", packageName)
if (importPath){
return `${prefix}${packageName}${importPath}`
}
return `${prefix}${packageName}${defaultPath}`
}
)
return result
}
================================================
FILE: .storybook/mocks/Meteor.js
================================================
// FIXME: we can't use ES6 imports in mocks, not sure why
module.exports = {
settings: {},
startup: () => { },
_localStorage: window ? window.localStorage : { setItem: () => {}, getItem: () => {} },
isClient: () => true,
isServer: () => false,
absoluteUrl: () => 'http://vulcanjs.org/'
}
================================================
FILE: .storybook/mocks/Mongo.js
================================================
module.exports = {
Collection: class Collection {}
}
================================================
FILE: .storybook/mocks/Vulcan.js
================================================
module.exports = {
}
================================================
FILE: .storybook/mocks/meteor-apollo.js
================================================
module.exports = {
MeteorAccountsLink: class MeteorAccountsLink {}
}
================================================
FILE: .storybook/mocks/meteor-server-render.js
================================================
module.exports = {
onPageLoad: () => { }
}
================================================
FILE: .storybook/mocks/vulcan-email.js
================================================
module.exports = {
addEmails: () => {}
}
================================================
FILE: .storybook/startup.js
================================================
/**
* Allow to run callbacks on Storybook startup, after stories are imported
* Based on Meteor.startup client side implementation
* @see https://github.com/meteor/meteor/blob/24865b28a0689de8b4949fb69ea1f95da647cd7a/packages/meteor/startup_client.js
*/
var callbackQueue = [];
var isLoadingCompleted = false;
var isReady = false;
// Keeps track of how many events to wait for in addition to loading completing,
// before we're considered ready.
var readyHoldsCount = 0;
var maybeReady = function () {
if (isReady || !isLoadingCompleted || readyHoldsCount > 0)
return;
isReady = true;
// Run startup callbacks
while (callbackQueue.length)
(callbackQueue.shift())();
};
var loadingCompleted = function () {
if (!isLoadingCompleted) {
isLoadingCompleted = true;
maybeReady();
}
}
if (document.readyState === 'complete' || document.readyState === 'loaded') {
// Loading has completed,
// but allow other scripts the opportunity to hold ready
window.setTimeout(loadingCompleted);
} else { // Attach event listeners to wait for loading to complete
if (document.addEventListener) {
document.addEventListener('DOMContentLoaded', loadingCompleted, false);
window.addEventListener('load', loadingCompleted, false);
} else { // Use IE event model for < IE9
document.attachEvent('onreadystatechange', function () {
if (document.readyState === "complete") {
loadingCompleted();
}
});
window.attachEvent('load', loadingCompleted);
}
}
/**
* @summary Run code when a client or a server starts.
* @locus Anywhere
* @param {Function} func A function to run on startup.
*/
const onStartup = function (callback) {
// Fix for < IE9, see http://javascript.nwbox.com/IEContentLoaded/
var doScroll = !document.addEventListener &&
document.documentElement.doScroll;
if (!doScroll || window !== top) {
if (isReady)
callback();
else
callbackQueue.push(callback);
} else {
try { doScroll('left'); }
catch (error) {
setTimeout(function () { onStartup(callback); }, 50);
return;
};
callback();
}
};
export default onStartup
================================================
FILE: .storybook/webpack.config.js
================================================
/*
Webpack setup
Adapt with your own loaders and config if necessary
*/
const path = require('path');
const webpack = require('webpack');
// Find Vulcan install, should not be modified
/**
* Smart function to find Vulcan packages
*
* You can either provide a path to Vulcan as VULCAN_DIR env
* or set the METEOR_PACKAGE_DIR variable
*/
const findPathToVulcanPackages = () => {
// look for VULCAN_DIR env variable
if (process.env.VULCAN_DIR) return `${process.env.VULCAN_DIR}/packages`;
// look for METEOR_PACKAGE_DIRS variable
const rawPackageDirs = process.env.METEOR_PACKAGE_DIRS;
if (rawPackageDirs) {
const dirs = rawPackageDirs.split(':');
// Vulcan dir should be '/some-folder/Vulcan/packages'
const vulcanPackagesDir = dirs.find(dir => !!dir.match(/\/Vulcan\//));
if (vulcanPackagesDir) {
return vulcanPackagesDir;
}
console.log(`
Please either set the VULCAN_DIR variable to your Vulcan folder or
set METEOR_PACKAGE_DIRS to your /packages folder.
Fallback to default value: '../../Vulcan'.`);
}
// default value
return '../../Vulcan/packages';
};
// path to your Vulcan repo (see 2-repo install in docs)
const pathToVulcanPackages = path.resolve(__dirname, findPathToVulcanPackages());
module.exports = ({ config }) => {
// Define aliases. Allow to mock some packages.
config.resolve = {
...config.resolve,
// this way node_modules are always those of current project and not of Vulcan
alias: {
...config.resolve.alias,
// Vulcan Packages
'meteor/vulcan:email': path.resolve(__dirname, './mocks/vulcan-email'),
//'meteor/vulcan:i18n': 'react-intl',
// Other packages
'meteor/apollo': path.resolve(__dirname, './mocks/meteor-apollo'),
'meteor/server-render': path.resolve(__dirname, './mocks/meteor-server-render'),
},
};
// Mock global variables
config.plugins.push(
new webpack.ProvidePlugin({
// mock global variables
Meteor: path.resolve(__dirname, './mocks/Meteor'),
Vulcan: path.resolve(__dirname, './mocks/Vulcan'),
Mongo: path.resolve(__dirname, './mocks/Mongo'),
_: 'underscore',
})
);
// force the config to use local node_modules instead the modules from Vulcan install
// Should not be modified
config.resolve.modules.push(path.resolve(__dirname, '../node_modules'));
// handle meteor packages
// Add your custom loaders here if necessary
config.module.rules.push({
test: /\.(js|jsx)$/,
exclude: /node_modules/,
loaders: [
// Remove meteor package (last step)
{
loader: 'scrap-meteor-loader',
options: {
// those package will be preserved, we provide a mock instead
preserve: ['meteor/apollo', 'meteor/vulcan:email', 'meteor/server-render'],
},
},
// Load Vulcan core packages
{
loader: 'vulcan-loader',
options: {
vulcanPackagesDir: pathToVulcanPackages,
environment: 'client',
// those package are mocked using an alias instead or just ignored
exclude: ['meteor/vulcan:email', 'meteor/vulcan:accounts'],
},
},
// Add your loaders here for your own local vulcan-packages
// Example for Vulcan Starter:
{
loader: path.resolve(__dirname, './loaders/starter-example-loader'),
options: {
packagesDir: path.resolve(__dirname, '../packages'),
environment: 'client',
},
},
],
});
// Parse JSX files outside of Storybook directory
// Should not be modified
config.module.rules.push({
test: /\.(js|jsx)$/,
loaders: [
{
loader: 'babel-loader',
query: {
presets: [
'@babel/react',
{
plugins: [
'@babel/plugin-proposal-class-properties',
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-proposal-nullish-coalescing-operator',
],
},
],
},
},
],
});
// Parse SCSS files
// Should not be modfied
config.module.rules.push({
test: /\.scss$/,
loaders: ['style-loader', 'css-loader', 'sass-loader'],
// include: path.resolve(__dirname, "../")
});
// Return the altered config
return config;
};
================================================
FILE: .vscode/launch.json
================================================
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}"
}
]
}
================================================
FILE: .vulcan/.gitignore
================================================
bkp
package.json
================================================
FILE: .vulcan/prestart_vulcan.js
================================================
#!/usr/bin/env node
//Functions
var fs = require('fs');
function existsSync(filePath){
try{
fs.statSync(filePath);
}catch(err){
if(err.code == 'ENOENT') return false;
}
return true;
}
function copySync(origin,target){
try{
fs.writeFileSync(target, fs.readFileSync(origin));
}catch(err){
if(err.code == 'ENOENT') return false;
}
return true;
}
//Add Definition Colors
const chalk = require('chalk');
//Vulcan letters
console.log(chalk.gray(' ___ ___ '));
console.log(chalk.gray(' '+String.fromCharCode(92))+chalk.redBright(String.fromCharCode(92))+chalk.dim.yellow(String.fromCharCode(92))+chalk.gray(String.fromCharCode(92)+' /')+chalk.dim.yellow('/')+chalk.yellowBright('/')+chalk.gray('/'));
console.log(chalk.gray(' '+String.fromCharCode(92))+chalk.redBright(String.fromCharCode(92))+chalk.dim.yellow(String.fromCharCode(92))+chalk.gray(String.fromCharCode(92))+chalk.gray('/')+chalk.dim.yellow('/')+chalk.yellowBright('/')+chalk.gray('/ Vulcan.js'));
console.log(chalk.gray(' '+String.fromCharCode(92))+chalk.redBright(String.fromCharCode(92))+chalk.dim.yellow(String.fromCharCode(92))+chalk.dim.yellow('/')+chalk.yellowBright('/')+chalk.gray('/ The full-stack React+GraphQL framework'));
console.log(chalk.gray(' ──── '));
var os = require('os');
var exec = require('child_process').execSync;
var options = {
encoding: 'utf8'
};
//Check Meteor and install if not installed
var checker = exec("meteor --version", options);
if (!checker.includes("Meteor ")) {
console.log("Vulcan requires Meteor but it's not installed. Trying to Install...");
//Check platform
if (os.platform()=='darwin') {
//Mac OS platform
console.log("🌋 "+chalk.bold.yellow("Good news you have a Mac and we will install it now! }"));
console.log(exec("curl https://install.meteor.com/ | bash", options));
} else if (os.platform()=='linux') {
//GNU/Linux platform
console.log("🌋 "+chalk.bold.yellow("Good news you are on GNU/Linux platform and we will install Meteor now!"));
console.log(exec("curl https://install.meteor.com/ | bash", options));
} else if (os.platform()=='win32') {
//Windows NT platform
console.log("> "+chalk.bold.yellow("Oh no! you are on a Windows platform and you will need to install Meteor Manually!"));
console.log("> "+chalk.dim.yellow("Meteor for Windows is available at: ")+chalk.redBright("https://install.meteor.com/windows"));
process.exit(-1)
}
} else {
//Check exist file settings and create if not exist
if (!existsSync("settings.json")) {
console.log("> "+chalk.bold.yellow("Creating your own settings.json file...\n"));
if (!copySync("sample_settings.json","settings.json")) {
console.log("> "+chalk.bold.red("Error Creating your own settings.json file...check files and permissions\n"));
process.exit(-1);
}
}
console.log("> "+chalk.bold.yellow("Happy hacking with Vulcan!"));
console.log("> "+chalk.dim.yellow("The docs are available at: ")+chalk.redBright("http://docs.vulcanjs.org"));
}
================================================
FILE: .vulcan/prettier/index.js
================================================
/**
* 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);
}
================================================
FILE: .vulcan/shared/listChangedFiles.js
================================================
/**
* 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;
================================================
FILE: .vulcan/shared/pathsByLanguageVersion.js
================================================
/**
* 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,
};
================================================
FILE: .vulcan/update_package.js
================================================
#!/usr/bin/env node
/*
### Usage
Place Vulcan's package.json in .vulcan/package.json and run meteor npm run update-package-json form your project's folder.
You'll have to manually manage breaking updates (example, from ^2.0.1 to ^3.0.2).
### Features
- makes a backup of the project's package.json
- only merges dependencies, devDependencies and peerDependencies
- if full merge is successful, shows a list of updated versions
- will store vulcanVersion in package.json for future updates
*/
var fs = require('fs');
var mergePackages = require('@userfrosting/merge-package-dependencies');
var jsdiff = require('diff');
require('colors');
function diffPartReducer(accumulator, part) {
// green for additions, red for deletions
// grey for common parts
var color = part.added ? 'green' : (part.removed ? 'red' : 'grey');
return {
text: (accumulator.text || '') + part.value[color],
count: (accumulator.count || 0) + (part.added || part.removed ? 1 : 0),
};
}
// copied from sort-object-keys package
function sortObjectByKeyNameList(object, sortWith) {
var keys;
var sortFn;
if (typeof sortWith === 'function') {
sortFn = sortWith;
} else {
keys = sortWith;
}
return (keys || []).concat(Object.keys(object).sort(sortFn)).reduce(function(total, key) {
total[key] = object[key];
return total;
}, Object.create({}));
}
var appDirPath = './';
var vulcanDirPath = './.vulcan/';
if (!fs.existsSync(vulcanDirPath + 'package.json')) {
console.log('Could not find \'' + vulcanDirPath + 'package.json\'');
} else if (!fs.existsSync(appDirPath + 'package.json')) {
console.log('Could not find \'' + appDirPath + 'package.json\'');
} else {
var appPackageFile = fs.readFileSync(appDirPath + '/package.json');
var appPackageJson = JSON.parse(appPackageFile);
var vulcanPackageFile = fs.readFileSync(vulcanDirPath + 'package.json');
var vulcanPackageJson = JSON.parse(vulcanPackageFile);
if (appPackageJson.vulcanVersion) {
console.log(appPackageJson.name + '@' + appPackageJson.version +
' \'package.json\' will be updated from Vulcan@' + appPackageJson.vulcanVersion +
' to Vulcan@' + vulcanPackageJson.version +
' dependencies.');
} else {
console.log(appPackageJson.name + '@' + appPackageJson.version +
' \'package.json\' will be updated with Vulcan@' + vulcanPackageJson.version +
' dependencies.');
}
var backupDirPath = vulcanDirPath + 'bkp/';
if (!fs.existsSync(backupDirPath)) {
fs.mkdirSync(backupDirPath);
}
var backupFilePath = backupDirPath + 'package-' + Date.now() + '.json';
console.log('Saving a backup of \'' + appDirPath + 'package.json\' in \'' + backupFilePath + '\'');
fs.writeFileSync(backupFilePath, appPackageFile);
var updatedAppPackageJson = mergePackages.npm(
// IMPORTANT: parse again because mergePackages.npm mutates json
JSON.parse(appPackageFile),
[vulcanDirPath]
);
updatedAppPackageJson.vulcanVersion = vulcanPackageJson.version;
[
'dependencies',
'devDependencies',
'peerDependencies'
].forEach(function(key) {
if (updatedAppPackageJson[key]) {
updatedAppPackageJson[key] = sortObjectByKeyNameList(updatedAppPackageJson[key]);
}
const diff = jsdiff.diffJson(
sortObjectByKeyNameList(appPackageJson[key] || {}),
updatedAppPackageJson[key] || {}
).reduce(diffPartReducer, {});
if (diff.count) {
console.log('Changes in "' + key + '":');
console.log(diff.text);
} else {
console.log('No changes in "' + key + '".');
}
});
fs.writeFileSync(appDirPath + 'package.json', JSON.stringify(updatedAppPackageJson, null, ' '));
}
================================================
FILE: CHANGELOG.md
================================================
### Changelog
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [Unreleased](https://github.com/VulcanJS/Vulcan/compare/1.15.0...HEAD)
- Fixed issue #2664 + Fixed canUpdate on Card.jsx [`#2694`](https://github.com/VulcanJS/Vulcan/pull/2694)
- add to storybook plugin-proposal-optional-chaining to accept component.jsx [`#2662`](https://github.com/VulcanJS/Vulcan/pull/2662)
- Unify Server and Client ThemeProviders and pass apolloClient as prop [`#2691`](https://github.com/VulcanJS/Vulcan/pull/2691)
- Connector filter calbacks [`#2690`](https://github.com/VulcanJS/Vulcan/pull/2690)
- SmartForm minor bug fixes Jan 2020 [`#2687`](https://github.com/VulcanJS/Vulcan/pull/2687)
- Flash messages bug fixes [`#2692`](https://github.com/VulcanJS/Vulcan/pull/2692)
- add graphql.context callback [`#2689`](https://github.com/VulcanJS/Vulcan/pull/2689)
- Material UI minor changes 01-2021 [`#2685`](https://github.com/VulcanJS/Vulcan/pull/2685)
- Flash messages in reactive state [`#2683`](https://github.com/VulcanJS/Vulcan/pull/2683)
- Get string refactor and document [`#2681`](https://github.com/VulcanJS/Vulcan/pull/2681)
- Reactive State [`#2682`](https://github.com/VulcanJS/Vulcan/pull/2682)
- Material UI Number input [`#2684`](https://github.com/VulcanJS/Vulcan/pull/2684)
- Random [`#2675`](https://github.com/VulcanJS/Vulcan/pull/2675)
- getString Refactor And Document [`#2671`](https://github.com/VulcanJS/Vulcan/pull/2671)
- FormDescription to ui-bootstrap [`#2674`](https://github.com/VulcanJS/Vulcan/pull/2674)
- Query filters to mongo params [`#2672`](https://github.com/VulcanJS/Vulcan/pull/2672)
- Fix error message for app.operation_not_allowed [`#2673`](https://github.com/VulcanJS/Vulcan/pull/2673)
- Feature RTL support in locale [`#2670`](https://github.com/VulcanJS/Vulcan/pull/2670)
- React Node Support in getString() [`#2667`](https://github.com/VulcanJS/Vulcan/pull/2667)
- Form Component New Names Fix 2 [`#2666`](https://github.com/VulcanJS/Vulcan/pull/2666)
- Form Component New Names Fix [`#2665`](https://github.com/VulcanJS/Vulcan/pull/2665)
- Form Component New Names [`#2659`](https://github.com/VulcanJS/Vulcan/pull/2659)
- add init callback to smartform [`#2663`](https://github.com/VulcanJS/Vulcan/pull/2663)
- VerifyEmail Bug [`#2660`](https://github.com/VulcanJS/Vulcan/pull/2660)
- i18n Plural Support [`#2654`](https://github.com/VulcanJS/Vulcan/pull/2654)
- Material UI Minor Updates Nov 2020 [`#2655`](https://github.com/VulcanJS/Vulcan/pull/2655)
- only Write and log graphql schema when different from existing file [`#2653`](https://github.com/VulcanJS/Vulcan/pull/2653)
- update permissions for debug schemas [`#2652`](https://github.com/VulcanJS/Vulcan/pull/2652)
- Apollo Client 3 Import Fixes [`#2644`](https://github.com/VulcanJS/Vulcan/pull/2644)
- Material UI Minor Updates Oct 2020 [`#2649`](https://github.com/VulcanJS/Vulcan/pull/2649)
- Multi HoC/Hook Returned Props [`#2651`](https://github.com/VulcanJS/Vulcan/pull/2651)
- Move FormLabel to vulcan:ui-bootstrap [`#2645`](https://github.com/VulcanJS/Vulcan/pull/2645)
- Minor Formatting Oct 2020 [`#2646`](https://github.com/VulcanJS/Vulcan/pull/2646)
- Minor Form Bugs Oct 2020 [`#2647`](https://github.com/VulcanJS/Vulcan/pull/2647)
- Minor Data Layer Fixes Oct 2020 [`#2648`](https://github.com/VulcanJS/Vulcan/pull/2648)
- Add placeholder to the whitelist [`#2643`](https://github.com/VulcanJS/Vulcan/pull/2643)
- Bump lodash.merge from 4.6.1 to 4.6.2 [`#2629`](https://github.com/VulcanJS/Vulcan/pull/2629)
- Bump lodash.mergewith from 4.6.0 to 4.6.2 [`#2630`](https://github.com/VulcanJS/Vulcan/pull/2630)
- Use Symbol.for for vulcan-accounts const STATES [`#2637`](https://github.com/VulcanJS/Vulcan/pull/2637)
- Feature/http only cookie [`#2631`](https://github.com/VulcanJS/Vulcan/pull/2631)
- Upgrade to Meteor 1.10.2 [`#2604`](https://github.com/VulcanJS/Vulcan/pull/2604)
- Validate document permissions bug fix [`#2622`](https://github.com/VulcanJS/Vulcan/pull/2622)
- CurrentUserRefetch [`#2621`](https://github.com/VulcanJS/Vulcan/pull/2621)
- MaterialUiMinorChanges8 [`#2620`](https://github.com/VulcanJS/Vulcan/pull/2620)
- Bump lodash from 4.17.15 to 4.17.19 [`#2600`](https://github.com/VulcanJS/Vulcan/pull/2600)
- FormInputUrlMailSocialScrubbing [`#2606`](https://github.com/VulcanJS/Vulcan/pull/2606)
- MuiInputWithNoLabel [`#2612`](https://github.com/VulcanJS/Vulcan/pull/2612)
- MuiSuggestUpgrades [`#2611`](https://github.com/VulcanJS/Vulcan/pull/2611)
- TooltipButtonUpgrades [`#2610`](https://github.com/VulcanJS/Vulcan/pull/2610)
- MuiModalChanges [`#2609`](https://github.com/VulcanJS/Vulcan/pull/2609)
- SmartFormEnhancements [`#2607`](https://github.com/VulcanJS/Vulcan/pull/2607)
- MaterialUiMinorChanges7 [`#2605`](https://github.com/VulcanJS/Vulcan/pull/2605)
- Upgrade to MUI 4.11.0 [`#2603`](https://github.com/VulcanJS/Vulcan/pull/2603)
- MuiDatatableUpgrades [`#2619`](https://github.com/VulcanJS/Vulcan/pull/2619)
- MuiScrollTriggerUpgrades [`#2618`](https://github.com/VulcanJS/Vulcan/pull/2618)
- RegisterComponentJSDoc [`#2616`](https://github.com/VulcanJS/Vulcan/pull/2616)
- GraphQLComments [`#2615`](https://github.com/VulcanJS/Vulcan/pull/2615)
- SegmentClientFix [`#2614`](https://github.com/VulcanJS/Vulcan/pull/2614)
- GenericMutationWrappersResponse [`#2613`](https://github.com/VulcanJS/Vulcan/pull/2613)
- Bugfix/add correct selector to delete [`#2601`](https://github.com/VulcanJS/Vulcan/pull/2601)
- Feature/showother ui material [`#2592`](https://github.com/VulcanJS/Vulcan/pull/2592)
- Forms submit target verification [`#2568`](https://github.com/VulcanJS/Vulcan/pull/2568)
- add itemProperties openNested option for nested schema [`#2594`](https://github.com/VulcanJS/Vulcan/pull/2594)
- add new update check api to ui-material Card.jsx [`#2593`](https://github.com/VulcanJS/Vulcan/pull/2593)
- fix allowed values for radiogroup [`#2588`](https://github.com/VulcanJS/Vulcan/pull/2588)
- change cookie secure to false on localhost [`#2587`](https://github.com/VulcanJS/Vulcan/pull/2587)
- Jun2020BugFixes1 [`#2583`](https://github.com/VulcanJS/Vulcan/pull/2583)
- change Mui to expect handleChange in props [`#2577`](https://github.com/VulcanJS/Vulcan/pull/2577)
- bugfix - SmartForm cannot update nested input fields [`#2574`](https://github.com/VulcanJS/Vulcan/pull/2574)
- Handle options in FormComponents (fix #2578) [`#2578`](https://github.com/VulcanJS/Vulcan/issues/2578)
- use more robust operationName mock link [`6cd9ec5`](https://github.com/VulcanJS/Vulcan/commit/6cd9ec592df8e25ecb98b9227dcd52ff2ea41585)
- Small clean up; fix form delete [`03c41d6`](https://github.com/VulcanJS/Vulcan/commit/03c41d678b601a10363e7e416b2add5d52ca13aa)
- stop cors validation as early as possible to avoid conflicting checks [`fa42f6e`](https://github.com/VulcanJS/Vulcan/commit/fa42f6e0068bdaca8f5dfaed3d7e49f795581b80)
#### [1.15.0](https://github.com/VulcanJS/Vulcan/compare/1.14.1...1.15.0)
> 10 May 2020
- Bugfix/2550 readable fields with document [`#2564`](https://github.com/VulcanJS/Vulcan/pull/2564)
- feature/update react-helmet [`#2563`](https://github.com/VulcanJS/Vulcan/pull/2563)
- Add useSignout hook to the accounts package [`#2521`](https://github.com/VulcanJS/Vulcan/pull/2521)
- Enable OpenCRUD backwards compatibility [`#2559`](https://github.com/VulcanJS/Vulcan/pull/2559)
- Don't run `options` function until `loading` is false, and `data` is populated [`#2561`](https://github.com/VulcanJS/Vulcan/pull/2561)
- Account/auth features available outside DDP using GraphQL [`#2530`](https://github.com/VulcanJS/Vulcan/pull/2530)
- Use Meteor's version of npm over system default [`#2560`](https://github.com/VulcanJS/Vulcan/pull/2560)
- move muicheckboxgroup proptypes name and options to inputProperties a… [`#2554`](https://github.com/VulcanJS/Vulcan/pull/2554)
- add switch/checkbox alternative in material ui [`#2553`](https://github.com/VulcanJS/Vulcan/pull/2553)
- Bump acorn from 5.7.1 to 5.7.4 [`#2546`](https://github.com/VulcanJS/Vulcan/pull/2546)
- add Express + use bodyParserGraphQL [`#2532`](https://github.com/VulcanJS/Vulcan/pull/2532)
- Update to current valid strikethrough tags [`#2543`](https://github.com/VulcanJS/Vulcan/pull/2543)
- Use Meteor's version of npm over system default [`#2506`](https://github.com/VulcanJS/Vulcan/issues/2506)
- Log out GraphQL schema to schema.graphql on every app start (fix #2517) [`#2517`](https://github.com/VulcanJS/Vulcan/issues/2517)
- add stories for modals [`b6729de`](https://github.com/VulcanJS/Vulcan/commit/b6729dec65afe1de8c5ab489a03958ea6c0bf5f1)
- fix for same origin [`5bfb696`](https://github.com/VulcanJS/Vulcan/commit/5bfb696e18d44c2aa3f28f2a719e703ba5940d3e)
- fix install with Meteor 1.10 [`138fc7c`](https://github.com/VulcanJS/Vulcan/commit/138fc7c914f8a34cd327ee1dbc1bdcd8782d8597)
#### [1.14.1](https://github.com/VulcanJS/Vulcan/compare/1.14.0...1.14.1)
> 19 February 2020
- Enhancement/forms accessibility [`#2519`](https://github.com/VulcanJS/Vulcan/pull/2519)
- Add spanish translations [`#2518`](https://github.com/VulcanJS/Vulcan/pull/2518)
- Use multiQueryUpdater in update2 to properly sort/filter [`#2497`](https://github.com/VulcanJS/Vulcan/pull/2497)
- change tooltip aria-label [`#2515`](https://github.com/VulcanJS/Vulcan/pull/2515)
- fix call to sortBy in Datatable.jsx [`#2514`](https://github.com/VulcanJS/Vulcan/pull/2514)
- Bump handlebars from 4.0.11 to 4.3.0 [`#2491`](https://github.com/VulcanJS/Vulcan/pull/2491)
- Enhancement/mui radio [`#2503`](https://github.com/VulcanJS/Vulcan/pull/2503)
- fix material ui seletmultiple [`#2502`](https://github.com/VulcanJS/Vulcan/pull/2502)
- datatable fix for material ui [`#2493`](https://github.com/VulcanJS/Vulcan/pull/2493)
- Allow itemProperties to be passed on to FormComponent [`#2505`](https://github.com/VulcanJS/Vulcan/pull/2505)
- Prevent calling forEachDocumentField on fields without a schema [`#2504`](https://github.com/VulcanJS/Vulcan/pull/2504)
- Semantized classname [`#2499`](https://github.com/VulcanJS/Vulcan/pull/2499)
- fix field permission validation bug [`#2498`](https://github.com/VulcanJS/Vulcan/pull/2498)
- fix bugs in extendCollection (#16) [`#2496`](https://github.com/VulcanJS/Vulcan/pull/2496)
- fix bugs in extendCollection [`#16`](https://github.com/VulcanJS/Vulcan/pull/16)
- Remove ! when parsing typename [`#2468`](https://github.com/VulcanJS/Vulcan/pull/2468)
- Implement _is_null in mongoParams [`#2469`](https://github.com/VulcanJS/Vulcan/pull/2469)
- Jt/extend collection fix [`#2490`](https://github.com/VulcanJS/Vulcan/pull/2490)
- add connection remoteAdress in context [`#2488`](https://github.com/VulcanJS/Vulcan/pull/2488)
- Remove hardcoded head tag in template-web.browser [`#2485`](https://github.com/VulcanJS/Vulcan/pull/2485)
- Fix datatable-loading crash in the storybook [`#2482`](https://github.com/VulcanJS/Vulcan/pull/2482)
- Add GraphQLSchema.context to context in runGraphQL [`#2474`](https://github.com/VulcanJS/Vulcan/pull/2474)
- Pass full doc and document to validate to validateDocumentPermissions [`#2478`](https://github.com/VulcanJS/Vulcan/pull/2478)
- Add story to demonstrate ref forwarding in Vulcan Components [`#2473`](https://github.com/VulcanJS/Vulcan/pull/2473)
- Add a password component [`#2472`](https://github.com/VulcanJS/Vulcan/pull/2472)
- Add buildResult functionality to useCreate2 to directly access created document [`#2470`](https://github.com/VulcanJS/Vulcan/pull/2470)
- use apollo client in multiqueryupdater to broadcast to watched queries [`#2471`](https://github.com/VulcanJS/Vulcan/pull/2471)
- Rewrite client side date converter to handle nesting [`#2465`](https://github.com/VulcanJS/Vulcan/pull/2465)
- Field level permissions validation take nested objects into account (read, create, update) [`#2451`](https://github.com/VulcanJS/Vulcan/pull/2451)
- Feature/backoffice 1.14 [`#2467`](https://github.com/VulcanJS/Vulcan/pull/2467)
- Add Vulcan.generateGraphQLQueries to debug.js [`#2463`](https://github.com/VulcanJS/Vulcan/pull/2463)
- Omit username from schema validation after deleting it from options [`#2466`](https://github.com/VulcanJS/Vulcan/pull/2466)
- fix default_mutations check if user owns and belongs to authorised group [`#2460`](https://github.com/VulcanJS/Vulcan/pull/2460)
- Pass document to Users.isMemberOf in permissions.js in [`#2461`](https://github.com/VulcanJS/Vulcan/pull/2461)
- add back package-lock to avoid issues [`3dbd9d9`](https://github.com/VulcanJS/Vulcan/commit/3dbd9d9115af43fad79cf5de3e9742116bfd80b0)
- locked react version [`05fcc19`](https://github.com/VulcanJS/Vulcan/commit/05fcc19619362b80ba4d609d72a5ec373197cc97)
- remove explicit install of airbnb-js-shims after fix [`862028e`](https://github.com/VulcanJS/Vulcan/commit/862028e2dc0ca3f4d388a4eb7015000f1afe917c)
#### [1.14.0](https://github.com/VulcanJS/Vulcan/compare/1.13.5...1.14.0)
> 3 December 2019
- Add support for `itemProperties` in schemas [`#2456`](https://github.com/VulcanJS/Vulcan/pull/2456)
- check options.input.limit and loadMoreInc accepts input [`#2459`](https://github.com/VulcanJS/Vulcan/pull/2459)
- remove async to fix issue in mongoParams.js filterFunction [`#2458`](https://github.com/VulcanJS/Vulcan/pull/2458)
- Pass document to Users.isMemberOf in permissions.js [`#2454`](https://github.com/VulcanJS/Vulcan/pull/2454)
- Set non-null to false for data input type on update/upsert [`#2453`](https://github.com/VulcanJS/Vulcan/pull/2453)
- Feature/add redux startup [`#2442`](https://github.com/VulcanJS/Vulcan/pull/2442)
- add preventDefault to ui-material ModalTrigger [`#2450`](https://github.com/VulcanJS/Vulcan/pull/2450)
- add @material-ui packages [`#2447`](https://github.com/VulcanJS/Vulcan/pull/2447)
- testing getVariablesListFromCache, verifiying right variables are returned [`#2439`](https://github.com/VulcanJS/Vulcan/pull/2439)
- Check if results is non-emtpy, not just present [`#2437`](https://github.com/VulcanJS/Vulcan/pull/2437)
- split getVariablesListFromCache test in two [`#2435`](https://github.com/VulcanJS/Vulcan/pull/2435)
- prevent cacheUpdate from returning variables of a query similar name [`#2434`](https://github.com/VulcanJS/Vulcan/pull/2434)
- Bugfix/form component inner prop [`#2433`](https://github.com/VulcanJS/Vulcan/pull/2433)
- Update of outdated links in README.md [`#2411`](https://github.com/VulcanJS/Vulcan/pull/2411)
- Fixed links to changelog and migration documentation to [`#2413`](https://github.com/VulcanJS/Vulcan/pull/2413)
- Incorrect link to telescopeapp.org [`#2412`](https://github.com/VulcanJS/Vulcan/pull/2412)
- Condition '!operation' is always false at this point because it is redundant. [`#2426`](https://github.com/VulcanJS/Vulcan/pull/2426)
- Mailchimp [`#2425`](https://github.com/VulcanJS/Vulcan/pull/2425)
- Condition 'value === ''' is always false at this point [`#2427`](https://github.com/VulcanJS/Vulcan/pull/2427)
- Bugfix/close modal [`#2429`](https://github.com/VulcanJS/Vulcan/pull/2429)
- Add pluralize [`518ec70`](https://github.com/VulcanJS/Vulcan/commit/518ec70d88e24eb81a3a15c1b8a15e650d1b666c)
- Better handling of getting collection name/type name [`827daa4`](https://github.com/VulcanJS/Vulcan/commit/827daa4f8ea3f50ea817d712da06218a7ccc67fb)
- Make filtering async [`9b8bf5d`](https://github.com/VulcanJS/Vulcan/commit/9b8bf5d26d06d0c7f4da4e4c28550cb7e58e873c)
#### [1.13.5](https://github.com/VulcanJS/Vulcan/compare/1.13.3...1.13.5)
> 25 October 2019
- TooltipIconButton arialabel titletext bugfix [`#2414`](https://github.com/VulcanJS/Vulcan/pull/2414)
- Bugfix/get viewable fields [`#2418`](https://github.com/VulcanJS/Vulcan/pull/2418)
- Enhance/form stories [`#2410`](https://github.com/VulcanJS/Vulcan/pull/2410)
- Can deactivate SSR [`#2397`](https://github.com/VulcanJS/Vulcan/pull/2397)
- add story of card [`#2408`](https://github.com/VulcanJS/Vulcan/pull/2408)
- split Card.jsx in differents files [`#2402`](https://github.com/VulcanJS/Vulcan/pull/2402)
- populate.before callback [`#2405`](https://github.com/VulcanJS/Vulcan/pull/2405)
- Bugfix/datatable props [`#2401`](https://github.com/VulcanJS/Vulcan/pull/2401)
- Feature/stories marieqg & juliensl [`#2400`](https://github.com/VulcanJS/Vulcan/pull/2400)
- fix multiQuery update error with non null selector [`#2396`](https://github.com/VulcanJS/Vulcan/pull/2396)
- FormGroupHiddenProp [`#2386`](https://github.com/VulcanJS/Vulcan/pull/2386)
- MaterialUiMinorChanges6 [`#2385`](https://github.com/VulcanJS/Vulcan/pull/2385)
- single2, create2, update2, delete2, upsert2 with new input param [`11e6d50`](https://github.com/VulcanJS/Vulcan/commit/11e6d50367025765d70e4aa3a24db5964c9e0939)
- hocs mutations use input directly [`1988bea`](https://github.com/VulcanJS/Vulcan/commit/1988bea602b2a9f263a751017272222295f2a291)
- bump 1.13.5 [`d844caa`](https://github.com/VulcanJS/Vulcan/commit/d844caa557f333508c19675a8462cdf80dae51d2)
#### [1.13.3](https://github.com/VulcanJS/Vulcan/compare/1.13.2...1.13.3)
> 7 October 2019
- Bugfix/watched mutations [`#2382`](https://github.com/VulcanJS/Vulcan/pull/2382)
- Material-UI - README Installation - Set Version of react-jss [`#2384`](https://github.com/VulcanJS/Vulcan/pull/2384)
- Ensure props that can be applied to the SmartForm component are propagated when said props are specified on the Card component. [`#2378`](https://github.com/VulcanJS/Vulcan/pull/2378)
- Hooks for mutations and other utilities [`#2377`](https://github.com/VulcanJS/Vulcan/pull/2377)
- MaterialUiStartAdornment [`#2376`](https://github.com/VulcanJS/Vulcan/pull/2376)
- MaterialUiMinorChanges5 [`#2372`](https://github.com/VulcanJS/Vulcan/pull/2372)
- ErrorsGetUserPayload [`#2373`](https://github.com/VulcanJS/Vulcan/pull/2373)
- Reenable data injection during SSR [`#2369`](https://github.com/VulcanJS/Vulcan/pull/2369)
- Bugfix/intl polyfill devel [`#2371`](https://github.com/VulcanJS/Vulcan/pull/2371)
- Feature/backoffice: merged devel [`#2366`](https://github.com/VulcanJS/Vulcan/pull/2366)
- Bugfix/mui ssr [`#2352`](https://github.com/VulcanJS/Vulcan/pull/2352)
- Feature/apollo register link [`#2353`](https://github.com/VulcanJS/Vulcan/pull/2353)
- Re-enable RouterHook component so that pageview events are sent to analytics [`#2364`](https://github.com/VulcanJS/Vulcan/pull/2364)
- MaterialUiMinorChanges6 [`b499708`](https://github.com/VulcanJS/Vulcan/commit/b499708cb3963344aa12faf70ac1ff337034ef42)
- add alerts to material ui datatable [`dc225e6`](https://github.com/VulcanJS/Vulcan/commit/dc225e6b59eb2d9b1980d82ae6d9c77e3e3194de)
- Add hasMany; add relation guessing based on schema field type [`c9518bc`](https://github.com/VulcanJS/Vulcan/commit/c9518bcbde7376f6efc0feba07c2459e81ef0b5c)
#### [1.13.2](https://github.com/VulcanJS/Vulcan/compare/1.13.1...1.13.2)
> 27 August 2019
- add to utils a getSchemaFieldAllowedValues function [`#2336`](https://github.com/VulcanJS/Vulcan/pull/2336)
- Update ThemeStyles.jsx [`#2337`](https://github.com/VulcanJS/Vulcan/pull/2337)
- Bugfix/button props [`#2357`](https://github.com/VulcanJS/Vulcan/pull/2357)
- add from props [`#2358`](https://github.com/VulcanJS/Vulcan/pull/2358)
- Fix bug in Utils.getUnusedSlug() [`#2359`](https://github.com/VulcanJS/Vulcan/pull/2359)
- EJSONStackOverflow [`#2348`](https://github.com/VulcanJS/Vulcan/pull/2348)
- lock storybook version to prevent compatibility issues [`d964c2c`](https://github.com/VulcanJS/Vulcan/commit/d964c2ce1f1b17f8b42fbc4dc3b1c38431925bdc)
- remove autogenerated doc folder [`3e9335a`](https://github.com/VulcanJS/Vulcan/commit/3e9335aed9b91dc810894442fb46efcbb6adcd72)
- cleanup [`53b8b8f`](https://github.com/VulcanJS/Vulcan/commit/53b8b8f688bc50448d7c8951ca1060222511f8f3)
#### [1.13.1](https://github.com/VulcanJS/Vulcan/compare/1.13.0...1.13.1)
> 24 July 2019
- MuiSuggestShowAllOptions [`#2346`](https://github.com/VulcanJS/Vulcan/pull/2346)
- VersionNumberCorrections [`#2345`](https://github.com/VulcanJS/Vulcan/pull/2345)
- UiMaterialMinorFixes3 [`#2341`](https://github.com/VulcanJS/Vulcan/pull/2341)
- SimpleSchemaIntegerFieldType [`#2340`](https://github.com/VulcanJS/Vulcan/pull/2340)
- ApolloEngineSettings [`#2339`](https://github.com/VulcanJS/Vulcan/pull/2339)
- Migrate accounts components to React Router 4 [`#2327`](https://github.com/VulcanJS/Vulcan/pull/2327)
- Bugfix/changepwd ssr [`#2344`](https://github.com/VulcanJS/Vulcan/pull/2344)
- Add default group only if necessary [`#2331`](https://github.com/VulcanJS/Vulcan/pull/2331)
- Bugfix/material ui floating [`#2330`](https://github.com/VulcanJS/Vulcan/pull/2330)
- MinorBugInMuiSampleTheme [`#2328`](https://github.com/VulcanJS/Vulcan/pull/2328)
- FormGroupAdminsOnlyOption [`#2326`](https://github.com/VulcanJS/Vulcan/pull/2326)
- VulcanAdminMinorTweaks [`#2325`](https://github.com/VulcanJS/Vulcan/pull/2325)
- MaterialUiMinorBugs [`#2315`](https://github.com/VulcanJS/Vulcan/pull/2315)
- FormNestedArrayLayout [`#2314`](https://github.com/VulcanJS/Vulcan/pull/2314)
- Storybook+Avatar+Button [`#2317`](https://github.com/VulcanJS/Vulcan/pull/2317)
- user.create.async callback [`#2311`](https://github.com/VulcanJS/Vulcan/pull/2311)
- ResetStoreCallbackNotBeingRun [`#2313`](https://github.com/VulcanJS/Vulcan/pull/2313)
- MuiCheckboxGroup maxCount [`#2312`](https://github.com/VulcanJS/Vulcan/pull/2312)
- vulcan-ui-material bug fixes [`#2310`](https://github.com/VulcanJS/Vulcan/pull/2310)
- instantiateComponent with React element [`#2309`](https://github.com/VulcanJS/Vulcan/pull/2309)
- Email template vars [`#2294`](https://github.com/VulcanJS/Vulcan/pull/2294)
- Move getLabel from vulcan:intl to vulcan:lib [`#2307`](https://github.com/VulcanJS/Vulcan/pull/2307)
- MaterialUiFormGroupLayouts [`#2297`](https://github.com/VulcanJS/Vulcan/pull/2297)
- vulcan-lib: New function: componentExists(name) [`#2292`](https://github.com/VulcanJS/Vulcan/pull/2292)
- FormGroupOptionalParameters [`#2296`](https://github.com/VulcanJS/Vulcan/pull/2296)
- material-ui : i18n files were not imported [`#2298`](https://github.com/VulcanJS/Vulcan/pull/2298)
- inputProperties function parameters bug [`#2295`](https://github.com/VulcanJS/Vulcan/pull/2295)
- FormComponentInner calling an inexisting element (FormNested) [`#2306`](https://github.com/VulcanJS/Vulcan/pull/2306)
- FormNestedArray Footer modification via props fixing [`#2305`](https://github.com/VulcanJS/Vulcan/pull/2305)
- improve props cleanup [`#2304`](https://github.com/VulcanJS/Vulcan/pull/2304)
- Deprecated redux devtools function replaced [`#2302`](https://github.com/VulcanJS/Vulcan/pull/2302)
- merging [`#1`](https://github.com/VulcanJS/Vulcan/pull/1)
- Update FormNestedObject.jsx [`#2289`](https://github.com/VulcanJS/Vulcan/pull/2289)
- Fixed MuiCheckBoxGroup value computation [`#2300`](https://github.com/VulcanJS/Vulcan/pull/2300)
- Update FormNestedArrayLayout.jsx [`#2288`](https://github.com/VulcanJS/Vulcan/pull/2288)
- Feature/better form array [`#2291`](https://github.com/VulcanJS/Vulcan/pull/2291)
- [WIP] fix MUI inputProperties handling [`#2286`](https://github.com/VulcanJS/Vulcan/pull/2286)
- Add storybook, few fixes on FormError + Intl messages [`#2284`](https://github.com/VulcanJS/Vulcan/pull/2284)
- add consideration to default values on accounts extra fields [`#2282`](https://github.com/VulcanJS/Vulcan/pull/2282)
- Feature/backoffice [`#2276`](https://github.com/VulcanJS/Vulcan/pull/2276)
- ui-material: updated Datatable.jsx - header fix [`#2275`](https://github.com/VulcanJS/Vulcan/pull/2275)
- Fix a falsy-vs-undefined bug in getSetting [`#2274`](https://github.com/VulcanJS/Vulcan/pull/2274)
- make Datatable edit modal title customizable [`#2269`](https://github.com/VulcanJS/Vulcan/pull/2269)
- FormInnerComponent unify Bootstrap and Material UI API [`#2272`](https://github.com/VulcanJS/Vulcan/pull/2272)
- Additional tests [`#2262`](https://github.com/VulcanJS/Vulcan/pull/2262)
- Fix a crash when using addFields together with a recursive schema [`#2268`](https://github.com/VulcanJS/Vulcan/pull/2268)
- Fix typo that prevented FormNestedObjectLayout from having error class [`#2265`](https://github.com/VulcanJS/Vulcan/pull/2265)
- Fix punctuation of password-change message [`#2264`](https://github.com/VulcanJS/Vulcan/pull/2264)
- Update synced-cron across a maintainer change [`#2263`](https://github.com/VulcanJS/Vulcan/pull/2263)
- Persian locale added [`#2261`](https://github.com/VulcanJS/Vulcan/pull/2261)
- Update mutators.js [`#2259`](https://github.com/VulcanJS/Vulcan/pull/2259)
- Unit Testing Update [`2357d82`](https://github.com/VulcanJS/Vulcan/commit/2357d826181271182cad4ee2bfa836946bc07e83)
- add material ui lib in dev mode [`de8e39c`](https://github.com/VulcanJS/Vulcan/commit/de8e39cd9834c450c5ecae6713b330fefaeda95f)
- StorybookAndMaterialUi [`f4c43a8`](https://github.com/VulcanJS/Vulcan/commit/f4c43a81133b07409b0cb334685fdba463ae8f4c)
#### [1.13.0](https://github.com/VulcanJS/Vulcan/compare/1.12.17...1.13.0)
> 28 March 2019
- Fix denormalization when bio set to empty [`#2253`](https://github.com/VulcanJS/Vulcan/pull/2253)
- Re-enable CheckboxGroup component for Material UI [`#2249`](https://github.com/VulcanJS/Vulcan/pull/2249)
- Re-enable Switch component for Material UI [`#2248`](https://github.com/VulcanJS/Vulcan/pull/2248)
- Re-enable Select component for Material UI [`#2250`](https://github.com/VulcanJS/Vulcan/pull/2250)
- Fix labeling in select component. [`#2243`](https://github.com/VulcanJS/Vulcan/pull/2243)
- Add debug messages for missing strings and default locale on per-message level. [`#2238`](https://github.com/VulcanJS/Vulcan/pull/2238)
- Add locale on signup. [`#2240`](https://github.com/VulcanJS/Vulcan/pull/2240)
- fix values property for handlebars helper, use context as default [`#2236`](https://github.com/VulcanJS/Vulcan/pull/2236)
- Add wrapperless Emails. [`#2235`](https://github.com/VulcanJS/Vulcan/pull/2235)
- Add default locale to getString. [`#2234`](https://github.com/VulcanJS/Vulcan/pull/2234)
- Upgrade react-router [`#2232`](https://github.com/VulcanJS/Vulcan/pull/2232)
- Fix const assignment. [`#2222`](https://github.com/VulcanJS/Vulcan/pull/2222)
- Add classname for required fields. [`#2215`](https://github.com/VulcanJS/Vulcan/pull/2215)
- fix a spelling error [`#2209`](https://github.com/VulcanJS/Vulcan/pull/2209)
- material-ui [`a743022`](https://github.com/VulcanJS/Vulcan/commit/a7430225458a18a3061496de760b93ebcd2a2c4b)
- Component fixes [`4261470`](https://github.com/VulcanJS/Vulcan/commit/4261470fada2b0da620f689c65b169d4aa5beff1)
- create vulcan:ui-material, update to Apollo2 RR4 [`b7b8725`](https://github.com/VulcanJS/Vulcan/commit/b7b872591a9ca35fe8b7d0dd36ef773f9e8d0af3)
#### [1.12.17](https://github.com/VulcanJS/Vulcan/compare/1.12.16...1.12.17)
> 16 February 2019
- Form test [`#2186`](https://github.com/VulcanJS/Vulcan/pull/2186)
- Fix type casting in FormComponent's handleChange (fix #2197) [`#2197`](https://github.com/VulcanJS/Vulcan/issues/2197)
- Show warning when core components are missing (fix #2196) [`#2196`](https://github.com/VulcanJS/Vulcan/issues/2196)
- prettier commit [`083a7d6`](https://github.com/VulcanJS/Vulcan/commit/083a7d676bc6f271223f4b293e1eb85731c4bea4)
- prettier & lint code [`815a65d`](https://github.com/VulcanJS/Vulcan/commit/815a65d853ae9ef0e4c9f8848723db4163ca44d1)
- prettier commit [`0f29cc7`](https://github.com/VulcanJS/Vulcan/commit/0f29cc7cdd7921dac3fb55fd93d56f81039558e1)
#### [1.12.16](https://github.com/VulcanJS/Vulcan/compare/1.12.15...1.12.16)
> 4 February 2019
#### [1.12.15](https://github.com/VulcanJS/Vulcan/compare/1.12.14...1.12.15)
> 4 February 2019
- Default view sort doesn't take precedence over selected view [`#2192`](https://github.com/VulcanJS/Vulcan/pull/2192)
- Fix users onCreate [`921e7cd`](https://github.com/VulcanJS/Vulcan/commit/921e7cd851149096c1dadc5a88c358912ca51515)
- Remove Formsy dependency [`f56293b`](https://github.com/VulcanJS/Vulcan/commit/f56293b0f6360f41412ffb497c4a9a84f6709db4)
#### [1.12.14](https://github.com/VulcanJS/Vulcan/compare/1.12.13...1.12.14)
> 31 January 2019
- Add script to update package.json dependencies [`#2174`](https://github.com/VulcanJS/Vulcan/pull/2174)
- Go back to using FormNestedFoot in Nested Array Fields [`#2170`](https://github.com/VulcanJS/Vulcan/pull/2170)
- Debug & Admin layouts [`#2177`](https://github.com/VulcanJS/Vulcan/pull/2177)
- Apollo2 finalization (backward compatibility) [`#2157`](https://github.com/VulcanJS/Vulcan/pull/2157)
- Use qs package [`40be66b`](https://github.com/VulcanJS/Vulcan/commit/40be66b8cf82d43e92d76493af3440027cd4617f)
- linting, removing apollo package [`eeada2d`](https://github.com/VulcanJS/Vulcan/commit/eeada2d231160d14f057cad32f919eeb2215eb35)
- Fixed versions issues (react-bootstrap + apollo) [`02cfafa`](https://github.com/VulcanJS/Vulcan/commit/02cfafabc2ce4c93dd2ab9beddd5def952482dd1)
#### [1.12.13](https://github.com/VulcanJS/Vulcan/compare/1.12.12...1.12.13)
> 2 January 2019
- Fix semicolons and other linting issues [`111e00e`](https://github.com/VulcanJS/Vulcan/commit/111e00ecae68a08a4f5c3a0321982f49c4be8c99)
- Fix ESLint unused variables [`3e1571e`](https://github.com/VulcanJS/Vulcan/commit/3e1571e1e8fdd1cf4b843713341b14c01e9c2545)
- Comment out sendy integration for now [`7ddcc28`](https://github.com/VulcanJS/Vulcan/commit/7ddcc28b97beb2f445b79fb6e12daef083eb9745)
#### [1.12.12](https://github.com/VulcanJS/Vulcan/compare/1.12.11...1.12.12)
> 31 December 2018
- Add ES translations for error messages [`#2165`](https://github.com/VulcanJS/Vulcan/pull/2165)
- forgot to add variable [`#2163`](https://github.com/VulcanJS/Vulcan/pull/2163)
- fixed nested array field error [`#2162`](https://github.com/VulcanJS/Vulcan/pull/2162)
- fixed helmet for testing [`#2156`](https://github.com/VulcanJS/Vulcan/pull/2156)
- fixed whitespace to pass linting test [`#2154`](https://github.com/VulcanJS/Vulcan/pull/2154)
- Feature/smart form reset [`#2131`](https://github.com/VulcanJS/Vulcan/pull/2131)
- Add async hook to RouterHook and provide props as argument [`#2065`](https://github.com/VulcanJS/Vulcan/pull/2065)
- Cleanup Datatable / withComponents pattern [`#2126`](https://github.com/VulcanJS/Vulcan/pull/2126)
- Add Prettier and Husky [`#2130`](https://github.com/VulcanJS/Vulcan/pull/2130)
- Support form id attribute in SmartForm [`#2152`](https://github.com/VulcanJS/Vulcan/pull/2152)
- clean up NPM packages [`4f49350`](https://github.com/VulcanJS/Vulcan/commit/4f49350d709bdc2be63c3f6a6b6765fd2ce02756)
- Fix NPM vulnerabilities [`8d00e60`](https://github.com/VulcanJS/Vulcan/commit/8d00e6091d5bf89d234a6be7f845bde70f0f8ccf)
- add redux [`19df560`](https://github.com/VulcanJS/Vulcan/commit/19df560545c4bc607e6c99907a6bf0f732c2dc68)
#### [1.12.11](https://github.com/VulcanJS/Vulcan/compare/1.12.10...1.12.11)
> 15 December 2018
- add minCount and maxCount to SmartForm nested arrays [`#2141`](https://github.com/VulcanJS/Vulcan/pull/2141)
- Allow user to customize apollo json parser options [`#2147`](https://github.com/VulcanJS/Vulcan/pull/2147)
- added collection creation hook [`#2148`](https://github.com/VulcanJS/Vulcan/pull/2148)
- add refetch to props on withSingle [`#2137`](https://github.com/VulcanJS/Vulcan/pull/2137)
- add english field errors [`#2136`](https://github.com/VulcanJS/Vulcan/pull/2136)
- Apollo 2 SSR [`#2128`](https://github.com/VulcanJS/Vulcan/pull/2128)
- Add Prettier and Husky [`b0f4ecd`](https://github.com/VulcanJS/Vulcan/commit/b0f4ecdae71a0f49c1270414a355f6c7dffd77a6)
- add bootstrap-ui to allow form mounting [`fde4d90`](https://github.com/VulcanJS/Vulcan/commit/fde4d90924ab3fae3e1673f6ab8cf04fbe4958c4)
- split Datatable into purely visual components [`7162870`](https://github.com/VulcanJS/Vulcan/commit/71628705646f3539a9489bad52d69f91fd8d5965)
#### [1.12.10](https://github.com/VulcanJS/Vulcan/compare/1.12.8...1.12.10)
> 24 November 2018
- Respect user.isAdmin on creation [`#2122`](https://github.com/VulcanJS/Vulcan/pull/2122)
- rollback callbacks test [`#2123`](https://github.com/VulcanJS/Vulcan/pull/2123)
- Mutation button small fixes [`#2116`](https://github.com/VulcanJS/Vulcan/pull/2116)
- Fix datatable bug when sorting a column [`#2113`](https://github.com/VulcanJS/Vulcan/pull/2113)
- single resolve documentId undefined -> now returns null [`#2112`](https://github.com/VulcanJS/Vulcan/pull/2112)
- Cleanup forms, add `changeCallback` [`#2108`](https://github.com/VulcanJS/Vulcan/pull/2108)
- set default email and siteName for Accounts related emails [`#2110`](https://github.com/VulcanJS/Vulcan/pull/2110)
- Warn when no searchable field is set and terms.query is set [`#2106`](https://github.com/VulcanJS/Vulcan/pull/2106)
- Pass context to the defaultView too [`#2109`](https://github.com/VulcanJS/Vulcan/pull/2109)
- Support arrays with primitives [`#2057`](https://github.com/VulcanJS/Vulcan/pull/2057)
- vulcan-form-tags: refactor and fix [`#2099`](https://github.com/VulcanJS/Vulcan/pull/2099)
- Update fr_FR.js: add datatable.search entry [`#2104`](https://github.com/VulcanJS/Vulcan/pull/2104)
- datatable: add i18n for the search field [`#2103`](https://github.com/VulcanJS/Vulcan/pull/2103)
- Revert changes made to getUnusedSlug in #2075 [`#2102`](https://github.com/VulcanJS/Vulcan/pull/2102)
- Custom form components [`#2080`](https://github.com/VulcanJS/Vulcan/pull/2080)
- [WIP] Apollo2 server [`#2094`](https://github.com/VulcanJS/Vulcan/pull/2094)
- Fix OpenCollective part of readme [`#2097`](https://github.com/VulcanJS/Vulcan/pull/2097)
- only clear current values for new document's form [`#2060`](https://github.com/VulcanJS/Vulcan/pull/2060)
- Restore Edge support [`#2093`](https://github.com/VulcanJS/Vulcan/pull/2093)
- SmartForm: use prop `schema`, if given [`#2092`](https://github.com/VulcanJS/Vulcan/pull/2092)
- Add /i18n debug page [`#2091`](https://github.com/VulcanJS/Vulcan/pull/2091)
- Users' slug is updated on displayname change [`#2075`](https://github.com/VulcanJS/Vulcan/pull/2075)
- SubmitButtonLabels [`#2082`](https://github.com/VulcanJS/Vulcan/pull/2082)
- Don't merge schema in Vulcan, only do it with SimpleSchema [`#2086`](https://github.com/VulcanJS/Vulcan/pull/2086)
- Email subjects shouldn't be coupled with sitename [`#2088`](https://github.com/VulcanJS/Vulcan/pull/2088)
- Apollo2 withMessages [`#2089`](https://github.com/VulcanJS/Vulcan/pull/2089)
- [WIP] Apollo 2: apollo-state-link and RR4 [`#2083`](https://github.com/VulcanJS/Vulcan/pull/2083)
- Support email headers property and simplify logic [`#2087`](https://github.com/VulcanJS/Vulcan/pull/2087)
- SmartForm.getLabel() intl string fallback [`#2077`](https://github.com/VulcanJS/Vulcan/pull/2077)
- Remove bootstrap from vulcan-forms [`#2076`](https://github.com/VulcanJS/Vulcan/pull/2076)
- Field resolvers should check for access (fix #2124) [`#2124`](https://github.com/VulcanJS/Vulcan/issues/2124)
- Revert some of the changes in #2112 (fix #2118) [`#2118`](https://github.com/VulcanJS/Vulcan/issues/2118)
- Pass query results to email's data() function as second argument (fix #2048) [`#2048`](https://github.com/VulcanJS/Vulcan/issues/2048)
- copy-pasted meteor/apollo, updated to RR4 [`2bacae7`](https://github.com/VulcanJS/Vulcan/commit/2bacae7a0e012caa99ecfab80d40206a09fe568b)
- Comment out/disable legacy code for now [`6275108`](https://github.com/VulcanJS/Vulcan/commit/6275108d41dab331991a339d65a0d57f6394d1df)
- basic example with apollo-server, not yet working [`8d3120a`](https://github.com/VulcanJS/Vulcan/commit/8d3120abc77b7cb65c4e9b1cd23280b87e302663)
#### [1.12.8](https://github.com/VulcanJS/Vulcan/compare/1.12.0...1.12.8)
> 17 September 2018
- fix bug preflight from apollo [`#2070`](https://github.com/VulcanJS/Vulcan/pull/2070)
- Pass locale as key to IntlProvider to force a rerender on locale change [`#2072`](https://github.com/VulcanJS/Vulcan/pull/2072)
- cleanup: Fix naming after withList -> withMulti change [`#2071`](https://github.com/VulcanJS/Vulcan/pull/2071)
- Added support for cc, bcc, and replyTo fields for email API [`#2062`](https://github.com/VulcanJS/Vulcan/pull/2062)
- [BugFix] remove `document` from `canCreateField` [`#2069`](https://github.com/VulcanJS/Vulcan/pull/2069)
- Minor bug fixes [`#2068`](https://github.com/VulcanJS/Vulcan/pull/2068)
- Allow "guests" in withAccess [`#2063`](https://github.com/VulcanJS/Vulcan/pull/2063)
- Minor changes and bug fixes in withMulti, single resolver, schema generation [`#2059`](https://github.com/VulcanJS/Vulcan/pull/2059)
- vulcan-users - permissions - filter out array based fields [`#2056`](https://github.com/VulcanJS/Vulcan/pull/2056)
- Cleaned up the options management in hocs [`#2053`](https://github.com/VulcanJS/Vulcan/pull/2053)
- Allow to edit username, and override 3rd party name if it is set [`#2052`](https://github.com/VulcanJS/Vulcan/pull/2052)
- Allow user to return nothing in submitCallback [`#2051`](https://github.com/VulcanJS/Vulcan/pull/2051)
- Pass collection as property when running new API mutators' callbacks [`#2050`](https://github.com/VulcanJS/Vulcan/pull/2050)
- fixed issue that hardcoded email test queries to work only with single queries [`#2047`](https://github.com/VulcanJS/Vulcan/pull/2047)
- Add only document once on withMulti query reducer [`#2049`](https://github.com/VulcanJS/Vulcan/pull/2049)
- Pass request headers through context [`#2046`](https://github.com/VulcanJS/Vulcan/pull/2046)
- When changing email address on an account, mark the new address as unverified [`#2043`](https://github.com/VulcanJS/Vulcan/pull/2043)
- Refactor registerComponent to fix #2061 (see also #2031) [`#2061`](https://github.com/VulcanJS/Vulcan/issues/2061)
- Fields with "$" should never be included in generated fragments (fix #2044) [`#2044`](https://github.com/VulcanJS/Vulcan/issues/2044)
- Fix ESLint [`5fc0e30`](https://github.com/VulcanJS/Vulcan/commit/5fc0e30f40183ae084e3344ec18c49ba1a1e866b)
- cleaned up the options management in hocs [`17f9671`](https://github.com/VulcanJS/Vulcan/commit/17f96712ff44ce9c86ebda0e14407a1f2f0d90f4)
- ESLint fixes [`dfa4c77`](https://github.com/VulcanJS/Vulcan/commit/dfa4c77314b9eaf7cb048d7a47512e30aad5ed2f)
#### [1.12.0](https://github.com/VulcanJS/Vulcan/compare/v1.11.1...1.12.0)
> 29 August 2018
- Add support for email address verification. [`#2040`](https://github.com/VulcanJS/Vulcan/pull/2040)
- defer creation of apolloClient until it is first used [`#2041`](https://github.com/VulcanJS/Vulcan/pull/2041)
- Users totalCount and removed one console.log [`#2035`](https://github.com/VulcanJS/Vulcan/pull/2035)
- OpenCRUD fixes [`#2034`](https://github.com/VulcanJS/Vulcan/pull/2034)
- [opencrud] update Users mutations to match {selector, data} args [`#2033`](https://github.com/VulcanJS/Vulcan/pull/2033)
- [Forms] Add currentDocument to clearForm [`#2030`](https://github.com/VulcanJS/Vulcan/pull/2030)
- Warn when replacing a non-registered component and register it anyway [`#2029`](https://github.com/VulcanJS/Vulcan/pull/2029)
- Minor default resolvers and mutations improvements [`#2028`](https://github.com/VulcanJS/Vulcan/pull/2028)
- Dynamic fragment initalization [`#2025`](https://github.com/VulcanJS/Vulcan/pull/2025)
- Pass opencrud field properties to the form [`#2024`](https://github.com/VulcanJS/Vulcan/pull/2024)
- Bugfix/nested objects b34f0a25ce0c775a29a14241b14e9bc0e47976c8 [`#2022`](https://github.com/VulcanJS/Vulcan/pull/2022)
- collection.getParameters handles schema extension for searchable fields [`#2021`](https://github.com/VulcanJS/Vulcan/pull/2021)
- Open Collective Updates [`#2020`](https://github.com/VulcanJS/Vulcan/pull/2020)
- Dynamic loader improvements [`#2013`](https://github.com/VulcanJS/Vulcan/pull/2013)
- FormComponent value handling improvements [`#2011`](https://github.com/VulcanJS/Vulcan/pull/2011)
- New default for Apollo tracing [`#2009`](https://github.com/VulcanJS/Vulcan/pull/2009)
- Remove hardcoded limit to users List resolver [`#2008`](https://github.com/VulcanJS/Vulcan/pull/2008)
- Fix async callbacks called with no arguments [`#2007`](https://github.com/VulcanJS/Vulcan/pull/2007)
- Allow passing multiple args to HOCs [`#2005`](https://github.com/VulcanJS/Vulcan/pull/2005)
- [vulcan:ui-bootstrap] fix duplicate "noneOption" on rerendering the Select component [`#2003`](https://github.com/VulcanJS/Vulcan/pull/2003)
- Fix #2027 [`#2027`](https://github.com/VulcanJS/Vulcan/issues/2027)
- Fix #1998 part 2 [`#1998`](https://github.com/VulcanJS/Vulcan/issues/1998)
- Fix #1998 [`#1998`](https://github.com/VulcanJS/Vulcan/issues/1998)
- Wrap checkboxgroup (fix #2004) [`#2004`](https://github.com/VulcanJS/Vulcan/issues/2004)
- Add debug statements and fix #2001 [`#2001`](https://github.com/VulcanJS/Vulcan/issues/2001)
- setup tests [`4744561`](https://github.com/VulcanJS/Vulcan/commit/474456148eddf72a33afe6faae92f4f38984e13f)
- setup an example test with Tinytest and added Jest's expect to dependencies [`6deab6b`](https://github.com/VulcanJS/Vulcan/commit/6deab6bb8f660582b2ddf541e004c0c012997425)
- Splitted FormNested between objects an arrays [`b5e54ea`](https://github.com/VulcanJS/Vulcan/commit/b5e54ead1733e361a2640e6a921579833d44603f)
#### [v1.11.1](https://github.com/VulcanJS/Vulcan/compare/1.9.1...v1.11.1)
> 13 June 2018
- runQuery->runGraphQL [`#1995`](https://github.com/VulcanJS/Vulcan/pull/1995)
- Popup warning on page closing for SmartForm unsaved changes [`#1989`](https://github.com/VulcanJS/Vulcan/pull/1989)
- Await between hooks in runCallbacks [`#1984`](https://github.com/VulcanJS/Vulcan/pull/1984)
- update FR translation for SmartForm changes [`#1986`](https://github.com/VulcanJS/Vulcan/pull/1986)
- Restrict value altering to allow multiple datatypes in FormComponent [`#1982`](https://github.com/VulcanJS/Vulcan/pull/1982)
- check for redirect before trying to redirect [`#1978`](https://github.com/VulcanJS/Vulcan/pull/1978)
- Add MongoDB aggregation to Collections [`#1961`](https://github.com/VulcanJS/Vulcan/pull/1961)
- withList Loading prop adjustment [`#1975`](https://github.com/VulcanJS/Vulcan/pull/1975)
- Include document prop in mutationErrorCallback method [`#1969`](https://github.com/VulcanJS/Vulcan/pull/1969)
- Update Flash component and withMessages container [`#1973`](https://github.com/VulcanJS/Vulcan/pull/1973)
- Update FormSubmit.jsx [`#1972`](https://github.com/VulcanJS/Vulcan/pull/1972)
- ui-bootstrap: Add a separate Modal component and refactor ModalTrigger to use it. [`#1971`](https://github.com/VulcanJS/Vulcan/pull/1971)
- Fix field errors display [`#1964`](https://github.com/VulcanJS/Vulcan/pull/1964)
- More graphql error logging [`#1965`](https://github.com/VulcanJS/Vulcan/pull/1965)
- change from telescope-newsletter to vulcan-newsletter [`#1966`](https://github.com/VulcanJS/Vulcan/pull/1966)
- Fix Form props [`#1960`](https://github.com/VulcanJS/Vulcan/pull/1960)
- Give CheckboxGroup its own custom event handler (fix #1998) [`#1998`](https://github.com/VulcanJS/Vulcan/issues/1998)
- Update packages [`c70f32a`](https://github.com/VulcanJS/Vulcan/commit/c70f32a52a94fe5f6d1dcc7700066a1a1c3ccf21)
- Changes to SmartForm behaviour [`c3f33cb`](https://github.com/VulcanJS/Vulcan/commit/c3f33cb7e0d623c89526b980904d35beaea0b7ad)
- Refactored GraphQL schema generation code to use new GraphQL templates [`1d39212`](https://github.com/VulcanJS/Vulcan/commit/1d3921287c2a1cad0ca52546c94ac1bc2491ad52)
#### [1.9.1](https://github.com/VulcanJS/Vulcan/compare/1.8.1...1.9.1)
> 21 April 2018
- Fix nested forms after props renaming [`#1959`](https://github.com/VulcanJS/Vulcan/pull/1959)
- add error_incorrect_password to FR package [`#1954`](https://github.com/VulcanJS/Vulcan/pull/1954)
- fix: connection -> connexion [`#1950`](https://github.com/VulcanJS/Vulcan/pull/1950)
- Vulcan FR language package [`#1943`](https://github.com/VulcanJS/Vulcan/pull/1943)
- use this.getCollection() instead of props.collection [`#1941`](https://github.com/VulcanJS/Vulcan/pull/1941)
- Update mingo to 2.2.0 [`#1928`](https://github.com/VulcanJS/Vulcan/pull/1928)
- Remove sanitizeHtml import on client [`#1925`](https://github.com/VulcanJS/Vulcan/pull/1925)
- vulcan:debug : Components Dashboard now shows all hocs [`#1923`](https://github.com/VulcanJS/Vulcan/pull/1923)
- Update es_ES.js [`#1918`](https://github.com/VulcanJS/Vulcan/pull/1918)
- Fix TrackerComponent and add missing i18n string [`#1913`](https://github.com/VulcanJS/Vulcan/pull/1913)
- Fix TrackerComponent and include in repo to fix Accounts.LoginFormInner [`#1907`](https://github.com/VulcanJS/Vulcan/pull/1907)
- Various minor bug fixes [`#1904`](https://github.com/VulcanJS/Vulcan/pull/1904)
- Fix Charges Insert [`#1902`](https://github.com/VulcanJS/Vulcan/pull/1902)
- Missing es-ES translation [`#1892`](https://github.com/VulcanJS/Vulcan/pull/1892)
- Add stripe callbacks [`#1887`](https://github.com/VulcanJS/Vulcan/pull/1887)
- Fix async register callback for vulcan-payments [`#1886`](https://github.com/VulcanJS/Vulcan/pull/1886)
- Fix stripe plan startup [`#1885`](https://github.com/VulcanJS/Vulcan/pull/1885)
- Create Stripe Subscriptions on startup if requested [`#1879`](https://github.com/VulcanJS/Vulcan/pull/1879)
- Export stripe singleton [`#1880`](https://github.com/VulcanJS/Vulcan/pull/1880)
- Datatable - replace SPACE with - in the datatable-item-* className [`#1868`](https://github.com/VulcanJS/Vulcan/pull/1868)
- Remove console log from routes dashboard [`#1877`](https://github.com/VulcanJS/Vulcan/pull/1877)
- Fix the Edit and Reply cancel methods on Comments component in example-forum package [`#1876`](https://github.com/VulcanJS/Vulcan/pull/1876)
- #1865 Fix upsert [`#1869`](https://github.com/VulcanJS/Vulcan/pull/1869)
- Fix example-forum events callback [`#1874`](https://github.com/VulcanJS/Vulcan/pull/1874)
- Fix embed for posts callback [`#1872`](https://github.com/VulcanJS/Vulcan/pull/1872)
- Export Cloudinary singleton [`#1873`](https://github.com/VulcanJS/Vulcan/pull/1873)
- Fix sample_settings.json for Google Analytics [`#1864`](https://github.com/VulcanJS/Vulcan/pull/1864)
- packages update to meteor 1.6.1 [`#1861`](https://github.com/VulcanJS/Vulcan/pull/1861)
- Add upsert mutation [`#1858`](https://github.com/VulcanJS/Vulcan/pull/1858)
- Fix linter errors [`#1857`](https://github.com/VulcanJS/Vulcan/pull/1857)
- CircleCI Config [`#1848`](https://github.com/VulcanJS/Vulcan/pull/1848)
- Respect checkAccess for total resolver [`#1853`](https://github.com/VulcanJS/Vulcan/pull/1853)
- optionsAsStrings was not defined [`#1855`](https://github.com/VulcanJS/Vulcan/pull/1855)
- Added getCollection to withEdit & withRemove [`#1851`](https://github.com/VulcanJS/Vulcan/pull/1851)
- Fix seeding not being sequential & fix broken example packages [`#1849`](https://github.com/VulcanJS/Vulcan/pull/1849)
- Added getCollection to withNew [`#1850`](https://github.com/VulcanJS/Vulcan/pull/1850)
- Fix React prop warnings for SmartForm [`#1847`](https://github.com/VulcanJS/Vulcan/pull/1847)
- Consolidate getCollection code [`#1844`](https://github.com/VulcanJS/Vulcan/pull/1844)
- revert allVotes [`#1843`](https://github.com/VulcanJS/Vulcan/pull/1843)
- Fix circular dependency between Utils and settings [`#1841`](https://github.com/VulcanJS/Vulcan/pull/1841)
- Allow SmartForm to use collection or collectionName [`#1840`](https://github.com/VulcanJS/Vulcan/pull/1840)
- Revert "showEdit on Card" [`#1839`](https://github.com/VulcanJS/Vulcan/pull/1839)
- Revert 1828 fix allvotes [`#1838`](https://github.com/VulcanJS/Vulcan/pull/1838)
- fix allVotes resolveAs [`#1828`](https://github.com/VulcanJS/Vulcan/pull/1828)
- Add package Spanish Translation i18n-es-es [`#1824`](https://github.com/VulcanJS/Vulcan/pull/1824)
- showEdit on Card [`#1836`](https://github.com/VulcanJS/Vulcan/pull/1836)
- Fix circular dependencies between schemas and collections [`#1837`](https://github.com/VulcanJS/Vulcan/pull/1837)
- Fixed graphQL schema for example-forum [`#1830`](https://github.com/VulcanJS/Vulcan/pull/1830)
- Fixed voting bugs [`#1827`](https://github.com/VulcanJS/Vulcan/pull/1827)
- Update to 1.8.5 [`#1`](https://github.com/VulcanJS/Vulcan/pull/1)
- Added comments from the example-simple video tutorial for reference f… [`#1820`](https://github.com/VulcanJS/Vulcan/pull/1820)
- Fix Newsletter Banner SSR [`#1817`](https://github.com/VulcanJS/Vulcan/pull/1817)
- Set default waiting state to false to avoid SSR override [`#1816`](https://github.com/VulcanJS/Vulcan/pull/1816)
- Use getComponent for childRoutes [`#1813`](https://github.com/VulcanJS/Vulcan/pull/1813)
- Allow the ApolloEngine LogLevel to be set via the settings [`#1810`](https://github.com/VulcanJS/Vulcan/pull/1810)
- Form loading state fix [`#1811`](https://github.com/VulcanJS/Vulcan/pull/1811)
- fix admin delete user hide bug [`#1803`](https://github.com/VulcanJS/Vulcan/pull/1803)
- Add VSCode jsconfig [`#1801`](https://github.com/VulcanJS/Vulcan/pull/1801)
- Debug Groups & Colors [`#1802`](https://github.com/VulcanJS/Vulcan/pull/1802)
- Fix warnings from React 16 update [`#1800`](https://github.com/VulcanJS/Vulcan/pull/1800)
- Upgrade to React 16.2 [`#1799`](https://github.com/VulcanJS/Vulcan/pull/1799)
- fix duplicate email returns "unknown error' [`#1795`](https://github.com/VulcanJS/Vulcan/pull/1795)
- Remove optics-agent from package.json [`#1791`](https://github.com/VulcanJS/Vulcan/pull/1791)
- Cleanup 1.8.1 [`#1790`](https://github.com/VulcanJS/Vulcan/pull/1790)
- Fix #1933 [`#1933`](https://github.com/VulcanJS/Vulcan/issues/1933)
- Move isAdmin init code to callback, fix #1917 [`#1917`](https://github.com/VulcanJS/Vulcan/issues/1917)
- Don't try to reorder items on vote (fix https://github.com/VulcanJS/Vulcan-Starter/issues/34) [`#34`](https://github.com/VulcanJS/Vulcan-Starter/issues/34)
- add missing await (fix https://github.com/VulcanJS/Vulcan-Starter/issues/24); get rid of extra db call [`#24`](https://github.com/VulcanJS/Vulcan-Starter/issues/24)
- Pass down props (fix #1856) [`#1856`](https://github.com/VulcanJS/Vulcan/issues/1856)
- Remove all example code from core repo (it lives in Starter repo now instead) [`7d17b57`](https://github.com/VulcanJS/Vulcan/commit/7d17b57f0591a820fbf41bf4d36a67562181c104)
- Package upgrade [`7ec4f4d`](https://github.com/VulcanJS/Vulcan/commit/7ec4f4ddd00c6e2f475d8ec5d21d669f5c33038e)
- Super-hacky fix to the accounts setState issue [`6facf15`](https://github.com/VulcanJS/Vulcan/commit/6facf15e17fec0eff0804d35360579513e7f586f)
#### [1.8.1](https://github.com/VulcanJS/Vulcan/compare/v1.7.0...1.8.1)
> 27 December 2017
- Add pre-validate callback on new user creation [`#1778`](https://github.com/VulcanJS/Vulcan/pull/1778)
- Fix issue #1770 [`#1771`](https://github.com/VulcanJS/Vulcan/pull/1771)
- Changes to support vulcan-material-ui [`#1772`](https://github.com/VulcanJS/Vulcan/pull/1772)
- Improve speed of vote score updates (Mongo aggregator + bulkwrite) [`#1759`](https://github.com/VulcanJS/Vulcan/pull/1759)
- Correct prescript for its operation in windows, making it compatible on all three platforms: linux, MacOS and Windows. [`#1754`](https://github.com/VulcanJS/Vulcan/pull/1754)
- Order of operation fix in scoring.js [`#1751`](https://github.com/VulcanJS/Vulcan/pull/1751)
- Added index to Votes on startup [`#1752`](https://github.com/VulcanJS/Vulcan/pull/1752)
- Update package.json [`#1747`](https://github.com/VulcanJS/Vulcan/pull/1747)
- Update README.md [`#1746`](https://github.com/VulcanJS/Vulcan/pull/1746)
- Bump the react-bootstrap version to get rid of "isMounted is deprecated..." error when opening modals [`#1744`](https://github.com/VulcanJS/Vulcan/pull/1744)
- Abstract out bootstrap-specific components in vulcan:forms [`#1750`](https://github.com/VulcanJS/Vulcan/pull/1750)
- Dropped isomorphic-fetch in favor of cross-fetch [`#1738`](https://github.com/VulcanJS/Vulcan/pull/1738)
- Update allow/deny package [`#1734`](https://github.com/VulcanJS/Vulcan/pull/1734)
- fixed some minor bugs in the documentation for subscribeMutationsGenerator [`#1728`](https://github.com/VulcanJS/Vulcan/pull/1728)
- Remove "unique" identifier file. [`#1723`](https://github.com/VulcanJS/Vulcan/pull/1723)
- Fix example simple [`#1724`](https://github.com/VulcanJS/Vulcan/pull/1724)
- Added submit option to `updateCurrentValues` in Form.jsx context [`#1722`](https://github.com/VulcanJS/Vulcan/pull/1722)
- Display errors on password reset form [`#1716`](https://github.com/VulcanJS/Vulcan/pull/1716)
- RegisterFragment should respect comments in fragment literals [`#1713`](https://github.com/VulcanJS/Vulcan/pull/1713)
- Update CONTRIBUTING.md [`#1712`](https://github.com/VulcanJS/Vulcan/pull/1712)
- [fix] give default value to unset [`#1703`](https://github.com/VulcanJS/Vulcan/pull/1703)
- Update sample_settings.json with new mailchimp schema properties [`#1698`](https://github.com/VulcanJS/Vulcan/pull/1698)
- Update CONTRIBUTING.md [`#1696`](https://github.com/VulcanJS/Vulcan/pull/1696)
- Clean up forum packages [`65eda4b`](https://github.com/VulcanJS/Vulcan/commit/65eda4b0334c96ab760ac4d084d722f971a62a24)
- refactoring example-forum package [`0a48d0c`](https://github.com/VulcanJS/Vulcan/commit/0a48d0ccbf282c03577b0a56b1cfeda74dda2a99)
- Dropped isomorphic-fetch in favor of cross-fetch (React Native compatible). [`30aad4c`](https://github.com/VulcanJS/Vulcan/commit/30aad4c2f543ca7fc575e97acbc86451e7ef379c)
#### [v1.7.0](https://github.com/VulcanJS/Vulcan/compare/v1.6.0...v1.7.0)
> 2 August 2017
- add function to remove tags from head [`#1678`](https://github.com/VulcanJS/Vulcan/pull/1678)
- Fix for Users.getTwitterName() [`#1683`](https://github.com/VulcanJS/Vulcan/pull/1683)
- #1658 Fix broken validation error Messages in LoginForm [`#1680`](https://github.com/VulcanJS/Vulcan/pull/1680)
- Fix remove permission strings [`#1673`](https://github.com/VulcanJS/Vulcan/pull/1673)
- create new example-permissions package to showcase groups & permissions API [`66e527f`](https://github.com/VulcanJS/Vulcan/commit/66e527fab461fa484d9a5bbf6251c9e588207f76)
- Add Membership example [`919ffaf`](https://github.com/VulcanJS/Vulcan/commit/919ffafab3cb1b4eb04ace99c42a1f5cbc5eb500)
- Added example-interfaces package [`2c6526f`](https://github.com/VulcanJS/Vulcan/commit/2c6526f81f78db913f2182323e42cfa45ef3f061)
#### [v1.6.0](https://github.com/VulcanJS/Vulcan/compare/v1.5.0...v1.6.0)
> 14 July 2017
- Added failure form callbacks and success form callbacks to forms [`#1666`](https://github.com/VulcanJS/Vulcan/pull/1666)
- (update_version): package:dymanic-import from 0.1.0 -> 0.1.1 to fix [`#1654`](https://github.com/VulcanJS/Vulcan/pull/1654)
- use correct code for Mailchimp "already subscribed" state [`#1651`](https://github.com/VulcanJS/Vulcan/pull/1651)
- (update_version): package:dymanic-import from 0.1.0 -> 0.1.1 to fix https://github.com/meteor/meteor/issues/8751 [`#8751`](https://github.com/meteor/meteor/issues/8751)
- Use default resolvers and mutations for instagram example [`31d0490`](https://github.com/VulcanJS/Vulcan/commit/31d0490a3697b44dc0a3239faa76cc74ab868200)
- add new default resolvers and default mutations; improve the way field resolvers are defined [`7ff1ada`](https://github.com/VulcanJS/Vulcan/commit/7ff1ada7d9d59ff4258ab727b526d7ca33191592)
- use new resolveAs syntax [`3345914`](https://github.com/VulcanJS/Vulcan/commit/334591450dc4242be2ad8ec8545a5eafaf976c77)
#### [v1.5.0](https://github.com/VulcanJS/Vulcan/compare/v1.2.0...v1.5.0)
> 12 June 2017
- Fixed typo in proptypes [`#1646`](https://github.com/VulcanJS/Vulcan/pull/1646)
- Fixed typo in formatMessage call [`#1645`](https://github.com/VulcanJS/Vulcan/pull/1645)
- Added PropType package and changed from component to PureComponent [`#1640`](https://github.com/VulcanJS/Vulcan/pull/1640)
- let addMediaAfterSubmit return post at the end [`#1633`](https://github.com/VulcanJS/Vulcan/pull/1633)
- Utils.getNestedProperty signature typo fix for email schema property [`#1630`](https://github.com/VulcanJS/Vulcan/pull/1630)
- fix bug where All Categories didn't clear the category filter [`#1616`](https://github.com/VulcanJS/Vulcan/pull/1616)
- Should call the property of parentRouteName properly [`#1612`](https://github.com/VulcanJS/Vulcan/pull/1612)
- Semi-colon Updates [`#1601`](https://github.com/VulcanJS/Vulcan/pull/1601)
- Workaround for Issue #1580 [`#1600`](https://github.com/VulcanJS/Vulcan/pull/1600)
- Enable facebook sharing of posts by supporting facebook scraper reqs [`#1596`](https://github.com/VulcanJS/Vulcan/pull/1596)
- small fix for bash install meteor [`#1597`](https://github.com/VulcanJS/Vulcan/pull/1597)
- Unable to assign category to a post (fix #1592) [`#1593`](https://github.com/VulcanJS/Vulcan/pull/1593)
- a make sense modified std:accounts-ui for telescope [`#1589`](https://github.com/VulcanJS/Vulcan/pull/1589)
- use npm simpl-schema, meteor aldeed:collection2-core package [`#1587`](https://github.com/VulcanJS/Vulcan/pull/1587)
- Fixed out-of-the-box newsletter config settings that causes constant … [`#1582`](https://github.com/VulcanJS/Vulcan/pull/1582)
- fix safari issues, remove defineName, improve apolloClientReducer to get the initialState [`#1583`](https://github.com/VulcanJS/Vulcan/pull/1583)
- add missing dependency (fix #1598) [`#1598`](https://github.com/VulcanJS/Vulcan/issues/1598)
- Merge pull request #1593 from comus/ss-patch [`#1592`](https://github.com/VulcanJS/Vulcan/issues/1592)
- Unable to assign category to a post (fix #1592) [`#1592`](https://github.com/VulcanJS/Vulcan/issues/1592)
- Include bootstrap CSS in movies example packages for now [`308280d`](https://github.com/VulcanJS/Vulcan/commit/308280d74904a2172824c260d6ef953737116818)
- Added PropTypes from 'prop-types'; [`14f5ba8`](https://github.com/VulcanJS/Vulcan/commit/14f5ba897175174f3d4d9e3f8734b967f213f8d8)
- vulcan:payments [`ca226ac`](https://github.com/VulcanJS/Vulcan/commit/ca226acca370818ef75c34613242685524dd16d6)
#### [v1.2.0](https://github.com/VulcanJS/Vulcan/compare/1.1.0...v1.2.0)
> 8 March 2017
- Adding Check and/or preinstall Meteor [`#1578`](https://github.com/VulcanJS/Vulcan/pull/1578)
- Nova 1.2.0 🚀 [`81c74db`](https://github.com/VulcanJS/Vulcan/commit/81c74db42fa9a4f1cc1ff7e2fb077153df7fdb36)
- experiment batching, let's see if it has a performance impact [`113b68f`](https://github.com/VulcanJS/Vulcan/commit/113b68f5edf73728cd3c9be6fe0bfd5b047c8d52)
- don't do "half-batching/caching", prepare for nova 1.2 [`0c19136`](https://github.com/VulcanJS/Vulcan/commit/0c19136298ab6519ddedb268184cd75416305e7a)
#### [1.1.0](https://github.com/VulcanJS/Vulcan/compare/v1.0.0...1.1.0)
> 16 February 2017
- better core/lib improvement and update [`#1569`](https://github.com/VulcanJS/Vulcan/pull/1569)
- renderContext fix [`#1565`](https://github.com/VulcanJS/Vulcan/pull/1565)
- Routing independent from deprecated packages & folder restructuration of nova:lib&core & update std:accounts-ui to 1.2.18 [`#1561`](https://github.com/VulcanJS/Vulcan/pull/1561)
- Add username tooltip to user's avatar [`#1555`](https://github.com/VulcanJS/Vulcan/pull/1555)
- new routing [`9992f00`](https://github.com/VulcanJS/Vulcan/commit/9992f0063ee6e73809400cbbe7c0bac153345c1f)
- work on notifications [`b67989f`](https://github.com/VulcanJS/Vulcan/commit/b67989fbc23f2b3b1f6e3ddca262de84af24476d)
- Adapt withEdit/withNew to support new fragments API [`afebadb`](https://github.com/VulcanJS/Vulcan/commit/afebadba55bf8d736a8028e11dae0086cbccfa7e)
### [v1.0.0](https://github.com/VulcanJS/Vulcan/compare/v0.27.5...v1.0.0)
> 2 February 2017
- separate client side and server side routing [`#1543`](https://github.com/VulcanJS/Vulcan/pull/1543)
- devel - revert commits related to simpl-schema [`#1537`](https://github.com/VulcanJS/Vulcan/pull/1537)
- #1517 - Implement configurable excerpt lengths [`#1536`](https://github.com/VulcanJS/Vulcan/pull/1536)
- fixed deployment instructions typo [`#1534`](https://github.com/VulcanJS/Vulcan/pull/1534)
- addRoute function improve, not use array.concat because it will retur… [`#1532`](https://github.com/VulcanJS/Vulcan/pull/1532)
- using nova:forms shows issues when being imported [`#1530`](https://github.com/VulcanJS/Vulcan/pull/1530)
- Allow redux middleware extensions [`#1528`](https://github.com/VulcanJS/Vulcan/pull/1528)
- default avatar image's URL for ssr [`#1526`](https://github.com/VulcanJS/Vulcan/pull/1526)
- adds pt-PT localization to the list [`#1521`](https://github.com/VulcanJS/Vulcan/pull/1521)
- fix #1541: increasePostViewCount mutation + associated resolver; store posts viewed on the client session on postsViewed in the redux store; document PostsPage HOC & lifecycle hook [`#1541`](https://github.com/VulcanJS/Vulcan/issues/1541)
- Pass Apollo client object to parameters callback to fix #1546 [`#1546`](https://github.com/VulcanJS/Vulcan/issues/1546)
- fix #1529 [`#1529`](https://github.com/VulcanJS/Vulcan/issues/1529)
- Nova 1.0.0 stable on master [`4baa939`](https://github.com/VulcanJS/Vulcan/commit/4baa9399256532bd4616037f490bad34e47913e3)
- Remove “__” prefix to avoid conflicts with GraphQL introspection types and simplify code [`db17e91`](https://github.com/VulcanJS/Vulcan/commit/db17e917f823ee8c5faae76adc2871f152bb379c)
- clean-up [`1c058b6`](https://github.com/VulcanJS/Vulcan/commit/1c058b60c68bfbdfadf864448a2764072a1b043c)
#### [v0.27.5](https://github.com/VulcanJS/Vulcan/compare/v0.27.4...v0.27.5)
> 30 November 2016
- v0.27.5 - really the latest full Meteor version [`#1518`](https://github.com/VulcanJS/Vulcan/pull/1518)
- eslint & clean up code, also fixed some bugs [`#1515`](https://github.com/VulcanJS/Vulcan/pull/1515)
- Newsletter subcription fixes [`#1513`](https://github.com/VulcanJS/Vulcan/pull/1513)
- npm run lint support for jsx files [`#1511`](https://github.com/VulcanJS/Vulcan/pull/1511)
- fix wrong comment in deep function [`#1512`](https://github.com/VulcanJS/Vulcan/pull/1512)
- clean up i18n files [`cbcfc1b`](https://github.com/VulcanJS/Vulcan/commit/cbcfc1bcafa5d1ffd0db9e81f24efaa499300282)
- clean up packages names [`d0c72c9`](https://github.com/VulcanJS/Vulcan/commit/d0c72c98f1d09f7bdbc25e8a38456e6791975229)
- adapt the `Telescope.createCollection` api to all the collections, some clean up in old containers files [`1137fb9`](https://github.com/VulcanJS/Vulcan/commit/1137fb96aa99456b454e0de72edc0a8f826ce507)
#### [v0.27.4](https://github.com/VulcanJS/Vulcan/compare/v0.27.3...v0.27.4)
> 15 November 2016
- v0.27.4 - latest version before Apollo official release [`#1508`](https://github.com/VulcanJS/Vulcan/pull/1508)
- add eslint with basic plugins and configuration. fixes #1470 [`#1474`](https://github.com/VulcanJS/Vulcan/pull/1474)
- Fix react setState race condition [`#1507`](https://github.com/VulcanJS/Vulcan/pull/1507)
- Only show comment reply button for logged in users [`#1504`](https://github.com/VulcanJS/Vulcan/pull/1504)
- Add zh-CN i18n package [`#1503`](https://github.com/VulcanJS/Vulcan/pull/1503)
- Add i18n messages for no more posts, no results, and load more days [`#1499`](https://github.com/VulcanJS/Vulcan/pull/1499)
- No comments.deleteById simulation for now [`#1497`](https://github.com/VulcanJS/Vulcan/pull/1497)
- Add Reset Password Feature [`#1491`](https://github.com/VulcanJS/Vulcan/pull/1491)
- fix typo in style class name [`#1487`](https://github.com/VulcanJS/Vulcan/pull/1487)
- meteor update npm-mongo ; meteor update mongo [`#1482`](https://github.com/VulcanJS/Vulcan/pull/1482)
- Merge pull request #1474 from moimikey/patch-1 [`#1470`](https://github.com/VulcanJS/Vulcan/issues/1470)
- modify getUnusedSlug to handle edge case on Users collection, fix #1501 and related to #1213 [`#1501`](https://github.com/VulcanJS/Vulcan/issues/1501)
- clean up callbacks by moving logic to mutations and schema (autoValue) [`8689a4d`](https://github.com/VulcanJS/Vulcan/commit/8689a4de73647bc949c51e9d1c90837cb52d7e22)
- refactoring PostsListContainer and CommentsListContainer into HoCs [`0ed0f24`](https://github.com/VulcanJS/Vulcan/commit/0ed0f24303219505e4ddbbff65aeedb1235a9520)
- move namespace to prefix on user schema: user.telescope.xxx by user.nova_xxx [`460efe5`](https://github.com/VulcanJS/Vulcan/commit/460efe52f606c1a77b745eb2ff61738cd0b0ac58)
#### [v0.27.3](https://github.com/VulcanJS/Vulcan/compare/v0.25.7...v0.27.3)
> 19 October 2016
- v0.27.3 [`#1475`](https://github.com/VulcanJS/Vulcan/pull/1475)
- Updated _posts.scss [`#1469`](https://github.com/VulcanJS/Vulcan/pull/1469)
- Clean some old code & fix some errors [`#1461`](https://github.com/VulcanJS/Vulcan/pull/1461)
- Add shortcut to submit form, close #1471 [`#1472`](https://github.com/VulcanJS/Vulcan/pull/1472)
- Update subscriberIdsToNotify to send unique emails [`#1466`](https://github.com/VulcanJS/Vulcan/pull/1466)
- Tell folks how to deploy Nova with latest Meteor Up (kadirahq/meteor-up), closes #1455 [`#1456`](https://github.com/VulcanJS/Vulcan/pull/1456)
- v0.27.2 [`#1454`](https://github.com/VulcanJS/Vulcan/pull/1454)
- Patch v0.27.1 proposal [`#1446`](https://github.com/VulcanJS/Vulcan/pull/1446)
- Added Brazilian Portuguese package to README.md [`#1444`](https://github.com/VulcanJS/Vulcan/pull/1444)
- Update groups.js (clarity) [`#1445`](https://github.com/VulcanJS/Vulcan/pull/1445)
- Update callback.js to include Linkedin [`#1432`](https://github.com/VulcanJS/Vulcan/pull/1432)
- new nova i18n package (de_DE) [`#1430`](https://github.com/VulcanJS/Vulcan/pull/1430)
- little detail makes big difference for nob like me [`#1428`](https://github.com/VulcanJS/Vulcan/pull/1428)
- nova:subscribe all the things [`#1425`](https://github.com/VulcanJS/Vulcan/pull/1425)
- Syntax highlighting added (where missing) ✨ [`#1424`](https://github.com/VulcanJS/Vulcan/pull/1424)
- Changing subscription method names & better error handling [`#1422`](https://github.com/VulcanJS/Vulcan/pull/1422)
- Extendable Subscribe component & locale [`#1412`](https://github.com/VulcanJS/Vulcan/pull/1412)
- Corrected imports for debug convenience globals [`#1420`](https://github.com/VulcanJS/Vulcan/pull/1420)
- Proposal for a CanDo Higher-Order Component [`#1417`](https://github.com/VulcanJS/Vulcan/pull/1417)
- Refactored subscribe-to-posts [`#1410`](https://github.com/VulcanJS/Vulcan/pull/1410)
- NovaForm: custom control has access to document as a props [`#1403`](https://github.com/VulcanJS/Vulcan/pull/1403)
- Nova i18n ru_RU package. [`#1392`](https://github.com/VulcanJS/Vulcan/pull/1392)
- pl_PL locale [`#1394`](https://github.com/VulcanJS/Vulcan/pull/1394)
- fixed types comparison in Posts.isApproved helper [`#1393`](https://github.com/VulcanJS/Vulcan/pull/1393)
- set locale in settings [`#1391`](https://github.com/VulcanJS/Vulcan/pull/1391)
- Fix Facebook settings error in sample_settings.json and README.md [`#1381`](https://github.com/VulcanJS/Vulcan/pull/1381)
- README: Remove duplicate in #optional-packages [`#1389`](https://github.com/VulcanJS/Vulcan/pull/1389)
- require react 15.0.x specifically [`#1385`](https://github.com/VulcanJS/Vulcan/pull/1385)
- Collection typo error in README.md [`#1380`](https://github.com/VulcanJS/Vulcan/pull/1380)
- don't do modification on a var if undefined => fixes #1375 error [`#1379`](https://github.com/VulcanJS/Vulcan/pull/1379)
- Meta SSR with react-helmet [`#1376`](https://github.com/VulcanJS/Vulcan/pull/1376)
- Different fixes [`#1373`](https://github.com/VulcanJS/Vulcan/pull/1373)
- Decouple components actions from Meteor [`#1370`](https://github.com/VulcanJS/Vulcan/pull/1370)
- added order support for custom fields [`#1364`](https://github.com/VulcanJS/Vulcan/pull/1364)
- use original PostsItem component [`#1362`](https://github.com/VulcanJS/Vulcan/pull/1362)
- 🐙 Fix custom package [`#1356`](https://github.com/VulcanJS/Vulcan/pull/1356)
- Hook up siteImage in settings to the open graph meta tags. [`#1342`](https://github.com/VulcanJS/Vulcan/pull/1342)
- Fixes - Episode II [`#1349`](https://github.com/VulcanJS/Vulcan/pull/1349)
- Fixes [`#1348`](https://github.com/VulcanJS/Vulcan/pull/1348)
- fix import statements in demo [`#1337`](https://github.com/VulcanJS/Vulcan/pull/1337)
- Newsletter + Mailchimp subscription enhancement [`#1332`](https://github.com/VulcanJS/Vulcan/pull/1332)
- Update README.md [`#1334`](https://github.com/VulcanJS/Vulcan/pull/1334)
- Complete soft delete feature for comments (Revision 2) [`#1323`](https://github.com/VulcanJS/Vulcan/pull/1323)
- Load categories at startup in load_categories.js [`#1324`](https://github.com/VulcanJS/Vulcan/pull/1324)
- Update README.md [`#1312`](https://github.com/VulcanJS/Vulcan/pull/1312)
- Only admin see post stats [`#1318`](https://github.com/VulcanJS/Vulcan/pull/1318)
- Fix social login anchor link [`#1311`](https://github.com/VulcanJS/Vulcan/pull/1311)
- fix 404Error page [`#1304`](https://github.com/VulcanJS/Vulcan/pull/1304)
- Completed profile hook [`#1301`](https://github.com/VulcanJS/Vulcan/pull/1301)
- Add siteUrl in front of action link. The link is wrong [`#1242`](https://github.com/VulcanJS/Vulcan/pull/1242)
- Move head tags to layout [`#1298`](https://github.com/VulcanJS/Vulcan/pull/1298)
- Helper: handle thumbnails from embedly, an external website or hosted on the app [`#1295`](https://github.com/VulcanJS/Vulcan/pull/1295)
- Fix HeadTags <-> Flexbox + add 2 helpers for images [`#1292`](https://github.com/VulcanJS/Vulcan/pull/1292)
- HeadTags: dochead instead of react-helmet [`#1291`](https://github.com/VulcanJS/Vulcan/pull/1291)
- Nova package updates [`#1287`](https://github.com/VulcanJS/Vulcan/pull/1287)
- Bugfixes for std:accounts-ui [`#1286`](https://github.com/VulcanJS/Vulcan/pull/1286)
- fixing typo in newCommentSubscribed notification [`#1279`](https://github.com/VulcanJS/Vulcan/pull/1279)
- update documentation link to skip readme.io welcome page [`#1276`](https://github.com/VulcanJS/Vulcan/pull/1276)
- Added current version 'fourseven:scss@3.4.1' to nova:share. [`#1269`](https://github.com/VulcanJS/Vulcan/pull/1269)
- update to version of alt:react-accounts* that doesn't depend on react-runtime [`#1264`](https://github.com/VulcanJS/Vulcan/pull/1264)
- [Nova] Update share package [`#1260`](https://github.com/VulcanJS/Vulcan/pull/1260)
- update alt:react-accounts for full SSR [`#1258`](https://github.com/VulcanJS/Vulcan/pull/1258)
- fix #1255 - add comment incrementing to Posts when adding a comment [`#1256`](https://github.com/VulcanJS/Vulcan/pull/1256)
- Fix Username already exists issue [403] [`#1252`](https://github.com/VulcanJS/Vulcan/pull/1252)
- re-add Dockerfile, fix #1477 [`#1477`](https://github.com/VulcanJS/Vulcan/issues/1477)
- add eslint with basic plugins and configuration. fixes #1470 [`#1470`](https://github.com/VulcanJS/Vulcan/issues/1470)
- Merge pull request #1472 from aszx87410/devel [`#1471`](https://github.com/VulcanJS/Vulcan/issues/1471)
- Add shortcut to submit form, close #1471 [`#1471`](https://github.com/VulcanJS/Vulcan/issues/1471)
- ensure user slug unicity, fixes #1213 [`#1213`](https://github.com/VulcanJS/Vulcan/issues/1213)
- add slug to newPendingPost notification, closes #1254 [`#1254`](https://github.com/VulcanJS/Vulcan/issues/1254)
- Merge pull request #1456 from asmita005/master [`#1455`](https://github.com/VulcanJS/Vulcan/issues/1455)
- complete license, fix #1117 [`#1117`](https://github.com/VulcanJS/Vulcan/issues/1117)
- fix #247 [`#247`](https://github.com/VulcanJS/Vulcan/issues/247)
- fix #1449 [`#1449`](https://github.com/VulcanJS/Vulcan/issues/1449)
- fix #1447, remove unnecessary load-script dependency [`#1447`](https://github.com/VulcanJS/Vulcan/issues/1447)
- fix #1423 [`#1423`](https://github.com/VulcanJS/Vulcan/issues/1423)
- require react 15.0.x specifically (fixes #1384) [`#1384`](https://github.com/VulcanJS/Vulcan/issues/1384)
- Merge pull request #1379 from xavcz/bang-bang-image-settings [`#1375`](https://github.com/VulcanJS/Vulcan/issues/1375)
- don't do modification on a var if undefined => fixes #1375 error at startup on HeadTags [`#1375`](https://github.com/VulcanJS/Vulcan/issues/1375)
- fix #1327 [`#1327`](https://github.com/VulcanJS/Vulcan/issues/1327)
- Merge pull request #1256 from paulmolluzzo/fix-comment-incrementing [`#1255`](https://github.com/VulcanJS/Vulcan/issues/1255)
- fix #1255 - add comment incrementing to Posts when adding a comment [`#1255`](https://github.com/VulcanJS/Vulcan/issues/1255)
- cleaning up nova:subscribe [`99a70a3`](https://github.com/VulcanJS/Vulcan/commit/99a70a326233b3cbdf251aaebbeab24e7a8d2b9f)
- clean up [`5a08bb6`](https://github.com/VulcanJS/Vulcan/commit/5a08bb634fa107b77ef9932c9ea886c3c2015a75)
- change old reference to AutoForm (legacy): field schema "autoform" -> "form" [`7775838`](https://github.com/VulcanJS/Vulcan/commit/7775838980d4c182d204312c03508a3c9c587b7e)
#### [v0.25.7](https://github.com/VulcanJS/Vulcan/compare/v0.25.5...v0.25.7)
> 6 February 2016
- supply default email based on 3rd party login, if possible [`#1223`](https://github.com/VulcanJS/Vulcan/pull/1223)
- Set counter name to category id instead of category slug [`#1229`](https://github.com/VulcanJS/Vulcan/pull/1229)
- Fixed url not defined in postPages[post.url] line 53 [`#1174`](https://github.com/VulcanJS/Vulcan/pull/1174)
- Fixing issue #1170 [`#1187`](https://github.com/VulcanJS/Vulcan/pull/1187)
- refactor permission code; make spam/pending/etc. posts unaccessible (fix #1219) [`#1219`](https://github.com/VulcanJS/Vulcan/issues/1219)
- make comment's postId uneditable in schema (fix #1231) [`#1231`](https://github.com/VulcanJS/Vulcan/issues/1231)
- Created and pushed by LingoHub. Project: 'Telescope' by User: 'hello@telescopeapp.org'. [`a247c5c`](https://github.com/VulcanJS/Vulcan/commit/a247c5cfc28c2a6efe7c331169bbb64666f7c467)
- fix i18n formatting [`6232923`](https://github.com/VulcanJS/Vulcan/commit/6232923904439eea2801baf47fd5f390b4bc1100)
- Created and pushed by LingoHub. Project: 'Telescope' by User: 'hello@telescopeapp.org'. [`bdc5c00`](https://github.com/VulcanJS/Vulcan/commit/bdc5c0056e7b71ac2e4dc9bca32bf9e73914ad88)
#### [v0.25.5](https://github.com/VulcanJS/Vulcan/compare/v0.25.4...v0.25.5)
> 28 October 2015
- Created and pushed by LingoHub. Project: 'Telescope' by User: 'hello@telescopeapp.org'. [`18b3f44`](https://github.com/VulcanJS/Vulcan/commit/18b3f4405115d7f87454a32de257d0f5e930473c)
- fix i18n syntax; add i18n files to pretender package [`5620d36`](https://github.com/VulcanJS/Vulcan/commit/5620d3670a826f8317872c081d6d2f5c0b756bba)
- version 0.25.5 [`fef1818`](https://github.com/VulcanJS/Vulcan/commit/fef1818f4ad778eef07be8ee597523f014760c5c)
#### [v0.25.4](https://github.com/VulcanJS/Vulcan/compare/v0.25.2...v0.25.4)
> 22 October 2015
- reformatting i18n files for tap:i18n compatibility [`f34b797`](https://github.com/VulcanJS/Vulcan/commit/f34b797fed8c83a5c5f20e63bb8f564d03ff7c56)
- version bump (0.25.3) [`c389514`](https://github.com/VulcanJS/Vulcan/commit/c38951414961650c314656ce1062379dcf887fb2)
- added telescope:prerender package [`eb8f7dc`](https://github.com/VulcanJS/Vulcan/commit/eb8f7dc141d8670f392fe7eac91c48d59c4330a4)
#### [v0.25.2](https://github.com/VulcanJS/Vulcan/compare/v0.25.0...v0.25.2)
> 16 October 2015
- Fixing `(error.error === 603)` always results false [`#1167`](https://github.com/VulcanJS/Vulcan/pull/1167)
- i18n.t bg translation + adding missing ones [`#1164`](https://github.com/VulcanJS/Vulcan/pull/1164)
- Fixes #1161 - Template.layout `pageName` should be reactive as route changes [`#1163`](https://github.com/VulcanJS/Vulcan/pull/1163)
- Fix e-mail template overrides by adding the "custom" prefix server-side [`#1159`](https://github.com/VulcanJS/Vulcan/pull/1159)
- replaced getUserName with getDisplayName for comments [`#1155`](https://github.com/VulcanJS/Vulcan/pull/1155)
- Fix bug that $set and $unset categories same time. [`#1152`](https://github.com/VulcanJS/Vulcan/pull/1152)
- Merge pull request #1163 from shilman/fix-1161 [`#1161`](https://github.com/VulcanJS/Vulcan/issues/1161)
- Fixes #1161 - Template.layout `pageName` should be reactive as route changes [`#1161`](https://github.com/VulcanJS/Vulcan/issues/1161)
- Fix bug that $set and $unset categories same time. [`#1150`](https://github.com/VulcanJS/Vulcan/issues/1150)
- Created and pushed by LingoHub. Project: 'Telescope' by User: 'hello@telescopeapp.org'. [`45ff625`](https://github.com/VulcanJS/Vulcan/commit/45ff62551068497996c0a42da975d23b9bf99fea)
- extracting menu component into its own package [`aed1f5a`](https://github.com/VulcanJS/Vulcan/commit/aed1f5a590757a1ce274630546deb2310c858237)
- move menu component to its own separate repo [`50633ff`](https://github.com/VulcanJS/Vulcan/commit/50633ff089fa5babf6339fe155fbb3f098b7f08e)
#### [v0.25.0](https://github.com/VulcanJS/Vulcan/compare/v0.24.0...v0.25.0)
> 24 September 2015
- Fix schema i18n by moving internationalize to collections [`#1115`](https://github.com/VulcanJS/Vulcan/pull/1115)
- Use abstraction of adminUsers consistently [`#1121`](https://github.com/VulcanJS/Vulcan/pull/1121)
- migrating to Flow Router (WIP) [`50c4874`](https://github.com/VulcanJS/Vulcan/commit/50c48745a30af6151902705c8c659e6488280342)
- Created and pushed by LingoHub. Project: 'Telescope' by User: 'hello@telescopeapp.org'. [`9b8d25d`](https://github.com/VulcanJS/Vulcan/commit/9b8d25d64f055bcf24c20f4c83a2afc6af2caaaf)
- sign-in/sign-up routes; clean up [`4894d5f`](https://github.com/VulcanJS/Vulcan/commit/4894d5f4f28e04934bc006c8d4d53b26ed1208e2)
#### [v0.24.0](https://github.com/VulcanJS/Vulcan/compare/v0.22.1...v0.24.0)
> 15 August 2015
- Fix Removing URL on Edit [`#1015`](https://github.com/VulcanJS/Vulcan/pull/1015)
- Show Share button on desktop version [`#1091`](https://github.com/VulcanJS/Vulcan/pull/1091)
- Correctly get url for sitemap using slug [`#1098`](https://github.com/VulcanJS/Vulcan/pull/1098)
- Added a RSS route that returns posts filtered by category [`#1100`](https://github.com/VulcanJS/Vulcan/pull/1100)
- make sure the postView is a function [`#1102`](https://github.com/VulcanJS/Vulcan/pull/1102)
- match anything (fix #1103) [`#1103`](https://github.com/VulcanJS/Vulcan/issues/1103)
- getDate -> date (fix #1092) [`#1092`](https://github.com/VulcanJS/Vulcan/issues/1092)
- fix #1009 [`#1009`](https://github.com/VulcanJS/Vulcan/issues/1009)
- stop using Session for search; do not trigger route redirect if search field is empty (fix #1063) [`#1063`](https://github.com/VulcanJS/Vulcan/issues/1063)
- give priority to field label over i18n string, if it exists (fix #1070) [`#1070`](https://github.com/VulcanJS/Vulcan/issues/1070)
- fix i18n son parsing issue [`83e5af6`](https://github.com/VulcanJS/Vulcan/commit/83e5af6b233493429a2ad8e32d8dfe7fab8b2c3a)
- Created and pushed by LingoHub. Project: 'Telescope' by User: 'hello@telescopeapp.org'. [`5fb9e8c`](https://github.com/VulcanJS/Vulcan/commit/5fb9e8c76f04f05b39e24cdf4925bdf36d3986c4)
- Created and pushed by LingoHub. Project: 'Telescope' by User: 'hello@telescopeapp.org'. [`d78b6b6`](https://github.com/VulcanJS/Vulcan/commit/d78b6b6fd0871a2c5fcb984dacb5d6483a3df2c5)
#### [v0.22.1](https://github.com/VulcanJS/Vulcan/compare/v0.21.1...v0.22.1)
> 27 July 2015
- use absolute URL for Users.getProfileUrl [`#1077`](https://github.com/VulcanJS/Vulcan/pull/1077)
- Title Links on Avatars [`#1067`](https://github.com/VulcanJS/Vulcan/pull/1067)
- allow hero modules to be full width of viewport [`#1065`](https://github.com/VulcanJS/Vulcan/pull/1065)
- Fix decrease inviteCount [`#1054`](https://github.com/VulcanJS/Vulcan/pull/1054)
- Added .startOf('day'); to `today` variable [`#1027`](https://github.com/VulcanJS/Vulcan/pull/1027)
- fixed syntax for passing in error type [`#1043`](https://github.com/VulcanJS/Vulcan/pull/1043)
- Display trimmed down version of htmlBody and fix #1069 [`#1069`](https://github.com/VulcanJS/Vulcan/issues/1069)
- add setting for pointing RSS links to discussion page; add pageUrl to API (fix #1038) [`#1038`](https://github.com/VulcanJS/Vulcan/issues/1038)
- version bump (0.22.1) [`1353f0a`](https://github.com/VulcanJS/Vulcan/commit/1353f0a74dc268e69d50967b31350e5c34402108)
- removing module template to simplify template structure [`b02b568`](https://github.com/VulcanJS/Vulcan/commit/b02b5688b31de281178d2ae901900c9ee7df53b9)
- Created and pushed by LingoHub. Project: 'Telescope' by User: 'hello@telescopeapp.org'. [`a8a8a61`](https://github.com/VulcanJS/Vulcan/commit/a8a8a61e09df050b22b04f8721d3c1f157b6690c)
#### [v0.21.1](https://github.com/VulcanJS/Vulcan/compare/v0.20.5...v0.21.1)
> 1 July 2015
- fix settings publication to hide private fields (take 2) [`#1024`](https://github.com/VulcanJS/Vulcan/pull/1024)
- Add Extra CSS [`#1019`](https://github.com/VulcanJS/Vulcan/pull/1019)
- Add option for each day of week for newsletter. Resolves #1034 [`#1034`](https://github.com/VulcanJS/Vulcan/issues/1034)
- Add Extra CSS settings field. Fixes #949 [`#949`](https://github.com/VulcanJS/Vulcan/issues/949)
- Return null if bootstrap-url is blank. Fixes #1012 [`#1012`](https://github.com/VulcanJS/Vulcan/issues/1012)
- Fix arrow key navigation for Single Day view. Fixes #986 [`#986`](https://github.com/VulcanJS/Vulcan/issues/986)
- Created and pushed by LingoHub. Project: 'Telescope-Test4' by User: 'hello@telescopeapp.org'. [`f80d9d8`](https://github.com/VulcanJS/Vulcan/commit/f80d9d85849e89b92b68df9dde81d04e941b811b)
- working on i18n [`1575aeb`](https://github.com/VulcanJS/Vulcan/commit/1575aeb43be981f40365e68e8b65c22e7e9da9bc)
- separating more languages [`a55f40c`](https://github.com/VulcanJS/Vulcan/commit/a55f40c36c41ae8b7cacd72ebee517ff73822948)
#### [v0.20.5](https://github.com/VulcanJS/Vulcan/compare/v0.15.1...v0.20.5)
> 9 June 2015
- Add ability to filter post views by category id [`#966`](https://github.com/VulcanJS/Vulcan/pull/966)
- Add Docker deployment support [`#962`](https://github.com/VulcanJS/Vulcan/pull/962)
- #959 - Fixed plural hardcoding issue by adding pointsUnitDisplayText … [`#960`](https://github.com/VulcanJS/Vulcan/pull/960)
- cosmetic remove some trailing commas from telescope-posts [`#961`](https://github.com/VulcanJS/Vulcan/pull/961)
- fix for nearly all of the getting started package issues [`#945`](https://github.com/VulcanJS/Vulcan/pull/945)
- Add topLevelCommentId field to comments. [`#943`](https://github.com/VulcanJS/Vulcan/pull/943)
- Improve jsHint consistency [`#934`](https://github.com/VulcanJS/Vulcan/pull/934)
- check post existence before access on postUsers publication [`#925`](https://github.com/VulcanJS/Vulcan/pull/925)
- Change color names in email package [`#908`](https://github.com/VulcanJS/Vulcan/pull/908)
- Fix #903 [`#905`](https://github.com/VulcanJS/Vulcan/pull/905)
- fix #1001 [`#1001`](https://github.com/VulcanJS/Vulcan/issues/1001)
- refactor views menu code to fix #1000 [`#1000`](https://github.com/VulcanJS/Vulcan/issues/1000)
- make subscribeUserOnCreation callback run asynchronously to fix #933 [`#933`](https://github.com/VulcanJS/Vulcan/issues/933)
- fix #974 [`#974`](https://github.com/VulcanJS/Vulcan/issues/974)
- fix #972 and fix uninvited users being allowed to post bug [`#972`](https://github.com/VulcanJS/Vulcan/issues/972)
- fix #952 [`#952`](https://github.com/VulcanJS/Vulcan/issues/952)
- fix #955 [`#955`](https://github.com/VulcanJS/Vulcan/issues/955)
- check post existence before access on postUsers publication [`#915`](https://github.com/VulcanJS/Vulcan/issues/915)
- Merge pull request #905 from saimeunt/devel [`#903`](https://github.com/VulcanJS/Vulcan/issues/903)
- Fix #903 [`#903`](https://github.com/VulcanJS/Vulcan/issues/903)
- Revert "Created and pushed by LingoHub. Project: 'Telescope' by User: 'hello@telescopeapp.org'." [`a6a904e`](https://github.com/VulcanJS/Vulcan/commit/a6a904e8c58f2e66eb0b1dd0d7e30230d28981a5)
- Created and pushed by LingoHub. Project: 'Telescope' by User: 'hello@telescopeapp.org'. [`caa7ae4`](https://github.com/VulcanJS/Vulcan/commit/caa7ae421dcbd42d707459d20df721b35a2dd5dc)
- owner -> member; set allow/deny for posts, comments, users [`fc8af1c`](https://github.com/VulcanJS/Vulcan/commit/fc8af1c9dac8c9affd4b7940c2e0f8eaf190631d)
#### [v0.15.1](https://github.com/VulcanJS/Vulcan/compare/v0.15.1-rc...v0.15.1)
> 9 April 2015
- adding pages package [`2b05abf`](https://github.com/VulcanJS/Vulcan/commit/2b05abf527936e2201d85b1d262b65f2f9a8ba47)
- collapse user menu by default [`cb7b416`](https://github.com/VulcanJS/Vulcan/commit/cb7b4164ab4d83edf3227525a19b7525de3bc9e1)
- don't display pages menu if there are no pages [`b7d3898`](https://github.com/VulcanJS/Vulcan/commit/b7d38982ff0e4b0b42e843741f51de52ef6a7b48)
#### [v0.15.1-rc](https://github.com/VulcanJS/Vulcan/compare/0.14.3...v0.15.1-rc)
> 8 April 2015
- Swedish translation [`#880`](https://github.com/VulcanJS/Vulcan/pull/880)
- Additional accessibility fixes [`#896`](https://github.com/VulcanJS/Vulcan/pull/896)
- Fix a bug where the post submit autoform hook wasn't called [`#883`](https://github.com/VulcanJS/Vulcan/pull/883)
- deleted unnecessary dot [`#884`](https://github.com/VulcanJS/Vulcan/pull/884)
- post-by-feed: Normalize encoding to utf-8 [`#882`](https://github.com/VulcanJS/Vulcan/pull/882)
- Unify posts hooks [`#875`](https://github.com/VulcanJS/Vulcan/pull/875)
- Update fr.i18n.json [`#867`](https://github.com/VulcanJS/Vulcan/pull/867)
- Improve comments loading performance on long threads [`#860`](https://github.com/VulcanJS/Vulcan/pull/860)
- Use this.userId, not Meteor.user in publications [`#855`](https://github.com/VulcanJS/Vulcan/pull/855)
- Add userId param to changeEmail method [`#854`](https://github.com/VulcanJS/Vulcan/pull/854)
- Validate post's categories on server. [`#835`](https://github.com/VulcanJS/Vulcan/pull/835)
- Fix: numberOfItemsInPast24Hours always returns 0 [`#830`](https://github.com/VulcanJS/Vulcan/pull/830)
- fix bug where last character in search keyword couldn't be cleared [`#833`](https://github.com/VulcanJS/Vulcan/pull/833)
- Added missing translations in Brazilian Portuguese [`#837`](https://github.com/VulcanJS/Vulcan/pull/837)
- missing `var` keyword for `defaultProperties` [`#827`](https://github.com/VulcanJS/Vulcan/pull/827)
- Fix #887 (thanks @kai101) [`#887`](https://github.com/VulcanJS/Vulcan/issues/887)
- Fix #892 (feeds not getting imported) [`#892`](https://github.com/VulcanJS/Vulcan/issues/892)
- Set canonical URL without overriding other params [`#878`](https://github.com/VulcanJS/Vulcan/issues/878)
- post-by-feed: Normalize encoding to utf-8 [`#729`](https://github.com/VulcanJS/Vulcan/issues/729)
- fix #868 [`#868`](https://github.com/VulcanJS/Vulcan/issues/868)
- Fix #822 [`#822`](https://github.com/VulcanJS/Vulcan/issues/822)
- update fourseven:sass (fix #859) [`#859`](https://github.com/VulcanJS/Vulcan/issues/859)
- updating AutoForm (fix #834) [`#834`](https://github.com/VulcanJS/Vulcan/issues/834)
- Use this.userId, not Meteor.user in publications [`#853`](https://github.com/VulcanJS/Vulcan/issues/853)
- Add userId param to changeEmail method [`#852`](https://github.com/VulcanJS/Vulcan/issues/852)
- Fix #754 [`#754`](https://github.com/VulcanJS/Vulcan/issues/754)
- Settings package [`057580b`](https://github.com/VulcanJS/Vulcan/commit/057580b7937c2cef3ce632fe3550fafca9ae2cc4)
- Translated to Swedish. [`208c154`](https://github.com/VulcanJS/Vulcan/commit/208c1546285f2c6119c95b745348b7669bcd124c)
- Update SEO package for master, remove page titles [`c3c8aab`](https://github.com/VulcanJS/Vulcan/commit/c3c8aab94a61e60b712d9df5be5cb8168e978d72)
#### [0.14.3](https://github.com/VulcanJS/Vulcan/compare/v0.14.2...0.14.3)
> 16 March 2015
- Refactor postAfterEditMethodCallbacks execution on server [`#814`](https://github.com/VulcanJS/Vulcan/pull/814)
- Correct base score calculation in vote.js [`#811`](https://github.com/VulcanJS/Vulcan/pull/811)
- Mailchimp limits email subject to 150 characters [`#783`](https://github.com/VulcanJS/Vulcan/pull/783)
- Fix broken twitter avatars (for real this time) [`#810`](https://github.com/VulcanJS/Vulcan/pull/810)
- Fix broken twitter avatars [`#809`](https://github.com/VulcanJS/Vulcan/pull/809)
- Improved vote accessability for packages [`#797`](https://github.com/VulcanJS/Vulcan/pull/797)
- Better spanish [`#795`](https://github.com/VulcanJS/Vulcan/pull/795)
- Add greek translation, fix english translation [`#794`](https://github.com/VulcanJS/Vulcan/pull/794)
- extraCode helper in layout template was not defined [`#785`](https://github.com/VulcanJS/Vulcan/pull/785)
- Various accessibility fixes, mainly hiding unnecessary elements from scr... [`#766`](https://github.com/VulcanJS/Vulcan/pull/766)
- update to bengott:avatar version 0.7.5 [`#799`](https://github.com/VulcanJS/Vulcan/pull/799)
- fix #790 [`#790`](https://github.com/VulcanJS/Vulcan/issues/790)
- add new Greek i18n translation [`1c920e4`](https://github.com/VulcanJS/Vulcan/commit/1c920e4d3648f2eba76a2de17a073f7a523df1c6)
- refactoring sidebar menu to use main nav [`d3665b9`](https://github.com/VulcanJS/Vulcan/commit/d3665b98959032471bb6aa0251cbc3ec0deca732)
- side nav prototype [`63e2aca`](https://github.com/VulcanJS/Vulcan/commit/63e2acabf5e2d9392528c876760794c06d0e448e)
#### [v0.14.2](https://github.com/VulcanJS/Vulcan/compare/v0.14.1...v0.14.2)
> 23 February 2015
- More bulgarian translations [`#781`](https://github.com/VulcanJS/Vulcan/pull/781)
- added swedish [`1952221`](https://github.com/VulcanJS/Vulcan/commit/19522215f7784cd825ac08d514c786130c727469)
- rebuild user management page with reactive-table [`8fd9de3`](https://github.com/VulcanJS/Vulcan/commit/8fd9de3266264c3cbeffdca959afb1ec60350745)
- auth methods are now a setting [`c8e1d60`](https://github.com/VulcanJS/Vulcan/commit/c8e1d608113f5fb90e04a77cae8ae72410d83fd0)
#### [v0.14.1](https://github.com/VulcanJS/Vulcan/compare/v0.14.0...v0.14.1)
> 11 February 2015
- Fixed CSS [`#753`](https://github.com/VulcanJS/Vulcan/pull/753)
- Typo fix [`#752`](https://github.com/VulcanJS/Vulcan/pull/752)
- Small improvements [`#748`](https://github.com/VulcanJS/Vulcan/pull/748)
- polish translation - only lang files [`#747`](https://github.com/VulcanJS/Vulcan/pull/747)
- Brazilian portuguese translation [`#744`](https://github.com/VulcanJS/Vulcan/pull/744)
- Vietnamese translation [`#736`](https://github.com/VulcanJS/Vulcan/pull/736)
- Fix Google Analytics [`#741`](https://github.com/VulcanJS/Vulcan/pull/741)
- Update tr.i18n.json [`#738`](https://github.com/VulcanJS/Vulcan/pull/738)
- Es translation missing for es.i18.json [`#735`](https://github.com/VulcanJS/Vulcan/pull/735)
- Fixes #719. Allowing mobile nav to close if user clicks anywhere outside of it [`#724`](https://github.com/VulcanJS/Vulcan/pull/724)
- Changed Sign-up to Register [`#726`](https://github.com/VulcanJS/Vulcan/pull/726)
- tweak button color specificity so that social sign-in button color is not affected (fix #481) [`#481`](https://github.com/VulcanJS/Vulcan/issues/481)
- fix #480 [`#480`](https://github.com/VulcanJS/Vulcan/issues/480)
- fix #699 [`#699`](https://github.com/VulcanJS/Vulcan/issues/699)
- *really* fix #743 [`#743`](https://github.com/VulcanJS/Vulcan/issues/743)
- Revert "fix #743" [`#743`](https://github.com/VulcanJS/Vulcan/issues/743)
- fix #743 [`#743`](https://github.com/VulcanJS/Vulcan/issues/743)
- fix #742 [`#742`](https://github.com/VulcanJS/Vulcan/issues/742)
- fix #737 [`#737`](https://github.com/VulcanJS/Vulcan/issues/737)
- Merge pull request #724 from anthonymayer/mobile-nav-click-outside [`#719`](https://github.com/VulcanJS/Vulcan/issues/719)
- Update _posts.scss [`a774b5b`](https://github.com/VulcanJS/Vulcan/commit/a774b5b2e4fcfbf118ec37d405e7ba771b4df60c)
- Translated to Brazilian Portuguese Completed [`34e7d0e`](https://github.com/VulcanJS/Vulcan/commit/34e7d0e113e859800f3fee8ec5d351a52dbfff55)
- Update vn.i18n.json [`eef8520`](https://github.com/VulcanJS/Vulcan/commit/eef8520c58d92e7b9cd481d73b4b3d090a90bdf6)
#### [v0.14.0](https://github.com/VulcanJS/Vulcan/compare/v0.14.0-rc...v0.14.0)
> 27 January 2015
- change sign in for register [`#722`](https://github.com/VulcanJS/Vulcan/pull/722)
- Adding newsletter time setting [`#712`](https://github.com/VulcanJS/Vulcan/pull/712)
- Update _posts.scss [`#716`](https://github.com/VulcanJS/Vulcan/pull/716)
- Fixes #719. Allowing mobile nav to close if user clicks anywhere outside of it. [`#719`](https://github.com/VulcanJS/Vulcan/issues/719)
- fixing mobile version for grid layout [`1aefbea`](https://github.com/VulcanJS/Vulcan/commit/1aefbea3cdd7cefc71bf6bd83710cb75ba4c640c)
- fix bug preventing posting comments [`a0ebc73`](https://github.com/VulcanJS/Vulcan/commit/a0ebc73cf7f5c0176ca935343d1892aa726ed933)
- fixing email notification templates [`c38c1c6`](https://github.com/VulcanJS/Vulcan/commit/c38c1c64348ac7aeb181a0c6a4481e389dcfd625)
#### [v0.14.0-rc](https://github.com/VulcanJS/Vulcan/compare/v0.13.0...v0.14.0-rc)
> 21 January 2015
- Cleaning up vote click handling functions and adding tests. [`#708`](https://github.com/VulcanJS/Vulcan/pull/708)
- Making both Travis and CodeClimate integrations works [`#706`](https://github.com/VulcanJS/Vulcan/pull/706)
- adding subscribe-to-posts package [`cf01d01`](https://github.com/VulcanJS/Vulcan/commit/cf01d01dbd242ebf21350dfb6d9a5afd8bbd2b6c)
- organising posts css [`b3804e4`](https://github.com/VulcanJS/Vulcan/commit/b3804e43ca26f00d29ff0a11468ebc6a3361b3c6)
- working on grid layout; added callback for injecting CSS classes for post items [`35ae630`](https://github.com/VulcanJS/Vulcan/commit/35ae630ebdd89e8667b449318e48a42104ad78ae)
#### [v0.13.0](https://github.com/VulcanJS/Vulcan/compare/v0.12.1...v0.13.0)
> 18 January 2015
- enabled trim and lowercase option for username field [`#696`](https://github.com/VulcanJS/Vulcan/pull/696)
- https is better [`#694`](https://github.com/VulcanJS/Vulcan/pull/694)
- Update posts.js [`#686`](https://github.com/VulcanJS/Vulcan/pull/686)
- Getting rid of redundant permissions functions. [`#672`](https://github.com/VulcanJS/Vulcan/pull/672)
- add html to .editorconfig [`#651`](https://github.com/VulcanJS/Vulcan/pull/651)
- Update helpers.js [`#677`](https://github.com/VulcanJS/Vulcan/pull/677)
- Add Bulgarian translation [`#669`](https://github.com/VulcanJS/Vulcan/pull/669)
- remove unnecessary decodeUrl (fix #675) [`#675`](https://github.com/VulcanJS/Vulcan/issues/675)
- Getting rid of redundant permissions functions [`f9d9891`](https://github.com/VulcanJS/Vulcan/commit/f9d9891fba27cfbb404f440c47ac06dc55b2c741)
- fixing newsletter sync/async issue [`1fd47b2`](https://github.com/VulcanJS/Vulcan/commit/1fd47b23f023aad730a1e89bd2ebf7911472a15f)
- rename files in singleDay package [`47ace39`](https://github.com/VulcanJS/Vulcan/commit/47ace39e26b492e1da19bbe064382732d181e756)
#### [v0.12.1](https://github.com/VulcanJS/Vulcan/compare/v0.12.0-pre...v0.12.1)
> 5 January 2015
- clean up packages [`bc048d2`](https://github.com/VulcanJS/Vulcan/commit/bc048d24d6612737b1cd5ed1e9ea91dedb060764)
- history & updated getting started [`67671d4`](https://github.com/VulcanJS/Vulcan/commit/67671d4ebd7122479a314d06eefb82c72b739611)
- disabling tests for now [`b75355d`](https://github.com/VulcanJS/Vulcan/commit/b75355d89db85712188d92bd3f7ce28b10ec9b31)
#### [v0.12.0](https://github.com/VulcanJS/Vulcan/compare/v0.11.1...v0.12.0)
> 3 January 2015
- Adding nav client unit test [`#662`](https://github.com/VulcanJS/Vulcan/pull/662)
- make primary and secondary nav sortable (fix #642) [`#642`](https://github.com/VulcanJS/Vulcan/issues/642)
- export PostsDigestController (fix #643) [`#643`](https://github.com/VulcanJS/Vulcan/issues/643)
- working on getting started package [`6a8a6ee`](https://github.com/VulcanJS/Vulcan/commit/6a8a6ee8bb007eeef31f46ad8e131ad18588164c)
- make release notes into a package [`ecad51b`](https://github.com/VulcanJS/Vulcan/commit/ecad51bbbd764405649829e5346b3f8a12dfcd11)
- clean-up [`778c08d`](https://github.com/VulcanJS/Vulcan/commit/778c08d544dd22d7b31bd1be79a43a9df13baeac)
#### [v0.12.0-pre](https://github.com/VulcanJS/Vulcan/compare/v0.12.0...v0.12.0-pre)
> 5 January 2015
- Add Bulgarian translation [`5ef1693`](https://github.com/VulcanJS/Vulcan/commit/5ef1693e44651541e536605f0219e243b9d6f54a)
- renaming viewNav to viewsMenu and adminNav to adminMenu [`f5354bf`](https://github.com/VulcanJS/Vulcan/commit/f5354bf69da2f592c999544fa65a3225a166fb57)
- css tweaks [`e789511`](https://github.com/VulcanJS/Vulcan/commit/e789511d8baeb752c4a559547d64b482a1f88ae5)
#### [v0.11.1](https://github.com/VulcanJS/Vulcan/compare/v0.11.0...v0.11.1)
> 29 December 2014
- fixed migrations.js when telescope-tags are removed [`#656`](https://github.com/VulcanJS/Vulcan/pull/656)
- Couple of small Newsletter fixes [`#655`](https://github.com/VulcanJS/Vulcan/pull/655)
- Update zh-CN.i18n.json [`#652`](https://github.com/VulcanJS/Vulcan/pull/652)
- Update to bengott:avatar@0.7.3 [`#647`](https://github.com/VulcanJS/Vulcan/pull/647)
- Update to bengott:avatar@0.7.2 [`#645`](https://github.com/VulcanJS/Vulcan/pull/645)
- Update to bengott:avatar@0.7.1 [`#644`](https://github.com/VulcanJS/Vulcan/pull/644)
- update to bengott:avatar@0.7.0 [`#640`](https://github.com/VulcanJS/Vulcan/pull/640)
- refactor voting code to accept function calls from server [`24a0f9b`](https://github.com/VulcanJS/Vulcan/commit/24a0f9b8306c8cfaf5d76840872e7cc672b419ba)
- subscribe post [`f6583aa`](https://github.com/VulcanJS/Vulcan/commit/f6583aad5e21d8541b70da7cadf59d9aabf1c536)
- working on post-by-feed package [`0b751d0`](https://github.com/VulcanJS/Vulcan/commit/0b751d086c52a9d59ab21eee6028349f7c5cd1d4)
#### [v0.11.0](https://github.com/VulcanJS/Vulcan/compare/v0.10.0...v0.11.0)
> 17 December 2014
- Add editorconfig for consistency [`#636`](https://github.com/VulcanJS/Vulcan/pull/636)
- Minor tweaks [`#634`](https://github.com/VulcanJS/Vulcan/pull/634)
- Russian translation [`#629`](https://github.com/VulcanJS/Vulcan/pull/629)
- Fix various url problems by taking siteUrl into account when getting route urls. [`#611`](https://github.com/VulcanJS/Vulcan/pull/611)
- fixed #631 [`#631`](https://github.com/VulcanJS/Vulcan/issues/631)
- fixed #632 - Update to useraccounts:unstyled@1.4.0 [`#632`](https://github.com/VulcanJS/Vulcan/issues/632)
- Auto post via RSS urls. Fixes #453 [`#453`](https://github.com/VulcanJS/Vulcan/issues/453)
- fix #617 [`#617`](https://github.com/VulcanJS/Vulcan/issues/617)
- Add link for clearing thumbnail (fix #607) [`#607`](https://github.com/VulcanJS/Vulcan/issues/607)
- use console.log() instead of throwing error to prevent post submit interruption (fix #607) [`#607`](https://github.com/VulcanJS/Vulcan/issues/607)
- fix digest parameters bug (fix #609) [`#609`](https://github.com/VulcanJS/Vulcan/issues/609)
- Update SEO package for master, remove page titles [`e25034c`](https://github.com/VulcanJS/Vulcan/commit/e25034c4db0c2776be7dd931d47ca929d21d133c)
- Refactor for getDescription and package style [`d17c447`](https://github.com/VulcanJS/Vulcan/commit/d17c447561b8722e562bb119d68f197b5b514638)
- Added Russian translation [`4331407`](https://github.com/VulcanJS/Vulcan/commit/4331407e7563d23608cda37b74ff247fbb4b7167)
#### [v0.10.0](https://github.com/VulcanJS/Vulcan/compare/v0.9.11...v0.10.0)
> 9 December 2014
- Switching from manually generating urls to using IronRouter functions. [`#588`](https://github.com/VulcanJS/Vulcan/pull/588)
- Fixes #444 - Adding UserEditController to show invites correctly [`#581`](https://github.com/VulcanJS/Vulcan/pull/581)
- Fixes #562 - Adds site link to email header. [`#587`](https://github.com/VulcanJS/Vulcan/pull/587)
- Look for settings in Meteor.settings too (fix #561) [`#561`](https://github.com/VulcanJS/Vulcan/issues/561)
- finish epic editor clean up and fix #591 [`#591`](https://github.com/VulcanJS/Vulcan/issues/591)
- don't need updateCategoryInPosts method anymore (fix #590) [`#590`](https://github.com/VulcanJS/Vulcan/issues/590)
- do not make call to CDN when language is english (fix #589) [`#589`](https://github.com/VulcanJS/Vulcan/issues/589)
- Merge pull request #581 from anthonymayer/invites-cleanup [`#444`](https://github.com/VulcanJS/Vulcan/issues/444)
- Merge pull request #587 from anthonymayer/email-header-site-link [`#562`](https://github.com/VulcanJS/Vulcan/issues/562)
- removing Epic Editor files [`17431df`](https://github.com/VulcanJS/Vulcan/commit/17431dfb8717df3fcc42a309321f9ca08db3affc)
- renaming errors to messages [`b6c54c1`](https://github.com/VulcanJS/Vulcan/commit/b6c54c106da4f72ee25e06c500c7d8a555d9c7c4)
- extracting digest into its own package [`75bd8d9`](https://github.com/VulcanJS/Vulcan/commit/75bd8d99201961f8d4039c6356701247d4f5d9da)
#### [v0.9.11](https://github.com/VulcanJS/Vulcan/compare/v0.9.10...v0.9.11)
> 3 December 2014
- Fixes #572 - Expands search box when focused or not empty. [`#580`](https://github.com/VulcanJS/Vulcan/pull/580)
- Fixes #543 - duplicate search logs. [`#571`](https://github.com/VulcanJS/Vulcan/pull/571)
- Hide mobile nav dropdowns [`#573`](https://github.com/VulcanJS/Vulcan/pull/573)
- Add Bulgarian-bg translation [`#558`](https://github.com/VulcanJS/Vulcan/pull/558)
- ru.i18n.json [`#557`](https://github.com/VulcanJS/Vulcan/pull/557)
- Compiling scss as part of build rather than with compass. [`#547`](https://github.com/VulcanJS/Vulcan/pull/547)
- Fixes #555. [`#556`](https://github.com/VulcanJS/Vulcan/pull/556)
- Fix telescope-search route for iron:router 1.0 [`#549`](https://github.com/VulcanJS/Vulcan/pull/549)
- tr.i18n.json [`#553`](https://github.com/VulcanJS/Vulcan/pull/553)
- Correcting emailNewPost template [`#554`](https://github.com/VulcanJS/Vulcan/pull/554)
- Fix #584 [`#584`](https://github.com/VulcanJS/Vulcan/issues/584)
- Fixes #444 - Adding UserEditController to show invites correctly [`#444`](https://github.com/VulcanJS/Vulcan/issues/444)
- Merge pull request #580 from anthonymayer/expanding-search-box [`#572`](https://github.com/VulcanJS/Vulcan/issues/572)
- Fixes #572 - Expands search box when focused or not empty. [`#572`](https://github.com/VulcanJS/Vulcan/issues/572)
- Merge pull request #571 from anthonymayer/fix-duplicate-search-logs [`#543`](https://github.com/VulcanJS/Vulcan/issues/543)
- Fixes #562 - Adds site link to email header. [`#562`](https://github.com/VulcanJS/Vulcan/issues/562)
- Merge pull request #556 from anthonymayer/missing_i18n_keys [`#555`](https://github.com/VulcanJS/Vulcan/issues/555)
- Fixes #555. [`#555`](https://github.com/VulcanJS/Vulcan/issues/555)
- create datetimepicker custom field type package [`9617639`](https://github.com/VulcanJS/Vulcan/commit/96176398e30fbfad700045faa2fee01602a61b25)
- working on post edit form [`6183716`](https://github.com/VulcanJS/Vulcan/commit/618371636de294af45d4486b8ed3902e075134f7)
- working on post submit form [`672be96`](https://github.com/VulcanJS/Vulcan/commit/672be96c9be7bbbd336b106163e7c2c57a984b5f)
#### [v0.9.10](https://github.com/VulcanJS/Vulcan/compare/v0.9.9-for-real...v0.9.10)
> 25 November 2014
- Upgrade to bengott:avatar 0.6.0 [`#548`](https://github.com/VulcanJS/Vulcan/pull/548)
- Fixes #541 [`#542`](https://github.com/VulcanJS/Vulcan/pull/542)
- Search webkit appearance [`#540`](https://github.com/VulcanJS/Vulcan/pull/540)
- Adding back title setting [`#537`](https://github.com/VulcanJS/Vulcan/pull/537)
- Merge pull request #542 from anthonymayer/filter-by-links-not-working [`#541`](https://github.com/VulcanJS/Vulcan/issues/541)
- Fixes #541 [`#541`](https://github.com/VulcanJS/Vulcan/issues/541)
- Fixes #538 in source scss, rather than in generated css [`#538`](https://github.com/VulcanJS/Vulcan/issues/538)
- Fixes #538 [`#538`](https://github.com/VulcanJS/Vulcan/issues/538)
- Compiling scss as part of build rather than with compass. [`30ca412`](https://github.com/VulcanJS/Vulcan/commit/30ca412921c28e5817cc1eb554d0591bac38039b)
- updating package versions [`f3ddf53`](https://github.com/VulcanJS/Vulcan/commit/f3ddf53cf7f25c15f12931c3e6e067019a192140)
- internationalizing packages [`0a696ce`](https://github.com/VulcanJS/Vulcan/commit/0a696ce1e3d8ac7ba20803625b6cccaa9a67a2b6)
#### [v0.9.9](https://github.com/VulcanJS/Vulcan/compare/v0.9.8...v0.9.9)
> 18 November 2014
- Splitting out router.js in multiple files. [`23079ff`](https://github.com/VulcanJS/Vulcan/commit/23079ff9f238ecd1b6f79558c7aea57e8254e73b)
- updating to Meteor 1.0 [`0ceda58`](https://github.com/VulcanJS/Vulcan/commit/0ceda58124bb5e0d30ed7f368196a1780825aa89)
- Working on IR 1.0 update [`73cb59a`](https://github.com/VulcanJS/Vulcan/commit/73cb59a088cd18180483aa823bc6227d203513b8)
#### [v0.9.9-for-real](https://github.com/VulcanJS/Vulcan/compare/v0.9.9...v0.9.9-for-real)
> 19 November 2014
- Convert translation keys format to tap:i18n standard [`2605dcb`](https://github.com/VulcanJS/Vulcan/commit/2605dcb27c514365933fe69271eeb0e94b8729b5)
- Convert lang js files to i18n.json [`c9c8f3e`](https://github.com/VulcanJS/Vulcan/commit/c9c8f3ea8df532244f1c9886e1ab85ee438dc1f9)
- refactor server-side email template routes [`eb08247`](https://github.com/VulcanJS/Vulcan/commit/eb082473ed0f0138591e1968f56a1c1dc2eadf57)
#### [v0.9.8](https://github.com/VulcanJS/Vulcan/compare/v0.9.7...v0.9.8)
> 18 October 2014
- Update to bengott:avatar 0.2.1 [`#493`](https://github.com/VulcanJS/Vulcan/pull/493)
- Fix email_hash bug (Issue #393) [`#491`](https://github.com/VulcanJS/Vulcan/pull/491)
- Update to bengott:avatar 0.1.4 [`#488`](https://github.com/VulcanJS/Vulcan/pull/488)
- Update to use bengott:avatar 0.1.2 [`#487`](https://github.com/VulcanJS/Vulcan/pull/487)
- Add missing adminMongoQuery and notAdminMongoQuery [`#472`](https://github.com/VulcanJS/Vulcan/pull/472)
- Add a Gitter chat badge to README.md [`#466`](https://github.com/VulcanJS/Vulcan/pull/466)
- Kadira package update to latest release 2.11.2 [`#469`](https://github.com/VulcanJS/Vulcan/pull/469)
- Update it.js [`#468`](https://github.com/VulcanJS/Vulcan/pull/468)
- Update to use bengott:avatar package for user avatars [`#454`](https://github.com/VulcanJS/Vulcan/pull/454)
- Add querystring updates to search [`#462`](https://github.com/VulcanJS/Vulcan/pull/462)
- Fully abstract isAdmin [`#463`](https://github.com/VulcanJS/Vulcan/pull/463)
- German translation (de.js) [`#458`](https://github.com/VulcanJS/Vulcan/pull/458)
- Posts rss refactor [`#450`](https://github.com/VulcanJS/Vulcan/pull/450)
- Hide future posts [`#449`](https://github.com/VulcanJS/Vulcan/pull/449)
- update to accounts-templates-unstyled 0.9.7 [`#448`](https://github.com/VulcanJS/Vulcan/pull/448)
- herald integration [`9be1bd7`](https://github.com/VulcanJS/Vulcan/commit/9be1bd7169ce407920019c2f8b65f791b2866a84)
- working on quick form for post submit [`ccf0ea7`](https://github.com/VulcanJS/Vulcan/commit/ccf0ea7820cadec85747b5c41e45f68c7fc2d34c)
- Make it possible to hide fields from quickform; cleanup [`73d1098`](https://github.com/VulcanJS/Vulcan/commit/73d1098646b45b943fb0f4f97c241ef77e896399)
#### [v0.9.7](https://github.com/VulcanJS/Vulcan/compare/v0.9.6...v0.9.7)
> 29 September 2014
- Avatar Tweaks [`#438`](https://github.com/VulcanJS/Vulcan/pull/438)
- Fixed issue that user would always be redirected to "/" after sign up and enables validation. [`#433`](https://github.com/VulcanJS/Vulcan/pull/433)
- Turn Gravatars from random helpers into a component [`#436`](https://github.com/VulcanJS/Vulcan/pull/436)
- Exclude posts scheduled in the future from the RSS feed [`#431`](https://github.com/VulcanJS/Vulcan/pull/431)
- fix #441 [`#441`](https://github.com/VulcanJS/Vulcan/issues/441)
- splitting settings form into field sets [`51de4d7`](https://github.com/VulcanJS/Vulcan/commit/51de4d79db807455ac1cda70fed9110928830e93)
- updating meteor [`95a2157`](https://github.com/VulcanJS/Vulcan/commit/95a21577686296176c6e114d18a498bd572d5f37)
- Adding instructions to settings form [`f00ffd8`](https://github.com/VulcanJS/Vulcan/commit/f00ffd8498f5fe6cc4da48d0d4e877c14c7237b5)
#### [v0.9.6](https://github.com/VulcanJS/Vulcan/compare/v0.9.5...v0.9.6)
> 26 September 2014
- Retinize gravatar image size [`#429`](https://github.com/VulcanJS/Vulcan/pull/429)
- comment rss [`#423`](https://github.com/VulcanJS/Vulcan/pull/423)
- fix #401 profile url collisions [`#420`](https://github.com/VulcanJS/Vulcan/pull/420)
- Publication validation [`#377`](https://github.com/VulcanJS/Vulcan/pull/377)
- Fix #430 [`#430`](https://github.com/VulcanJS/Vulcan/issues/430)
- Merge pull request #420 from GoodEveningMiss/slugify-collisions [`#401`](https://github.com/VulcanJS/Vulcan/issues/401)
- fix #401 [`#401`](https://github.com/VulcanJS/Vulcan/issues/401)
- adding telescope-kadira package [`b54b7b6`](https://github.com/VulcanJS/Vulcan/commit/b54b7b60d88917adcc6df777bd153b07d6e29323)
- working on CSS [`e04a4e9`](https://github.com/VulcanJS/Vulcan/commit/e04a4e98e3a25e7da117a6a27c0609fe29c102cf)
- finishing css tweaks [`25f5fcd`](https://github.com/VulcanJS/Vulcan/commit/25f5fcd778af1d5026bc73970ee5e64460cd7060)
#### [v0.9.5](https://github.com/VulcanJS/Vulcan/compare/v0.9.4...v0.9.5)
> 20 September 2014
- Fixes #415: prevent invalid up/downvotes when concurrent requests [`#416`](https://github.com/VulcanJS/Vulcan/pull/416)
- Corrected path to /forgot-password [`#414`](https://github.com/VulcanJS/Vulcan/pull/414)
- Update README.nitrous.md [`#412`](https://github.com/VulcanJS/Vulcan/pull/412)
- Update README.nitrous.md [`#411`](https://github.com/VulcanJS/Vulcan/pull/411)
- swap order of subtract() args due to deprecation [`#410`](https://github.com/VulcanJS/Vulcan/pull/410)
- Fix issue #403 - Replaced deprecated "schema" property with "attachSchema" method. [`#407`](https://github.com/VulcanJS/Vulcan/pull/407)
- cache jQuery; cleanup [`#404`](https://github.com/VulcanJS/Vulcan/pull/404)
- Merge pull request #416 from spifd/fix-concurrent-updownvotes [`#415`](https://github.com/VulcanJS/Vulcan/issues/415)
- Making notifications into their own package [`2a91121`](https://github.com/VulcanJS/Vulcan/commit/2a911217e9fb4637d66276e7d5e881a83b96fd86)
- added italian locales [`64018cb`](https://github.com/VulcanJS/Vulcan/commit/64018cbf427b55897e94a1905f3ea6c89ad36dfb)
- cleanup while getting familiar with the codebase [`6fc6b9e`](https://github.com/VulcanJS/Vulcan/commit/6fc6b9eb785d408dbde42db54e8176a743899e50)
#### v0.9.4
> 16 September 2014
- use UI.dynamic for incoming posts template [`#402`](https://github.com/VulcanJS/Vulcan/pull/402)
- Allow images in body. [`#397`](https://github.com/VulcanJS/Vulcan/pull/397)
- Correcting the if statement for profile.site url [`#396`](https://github.com/VulcanJS/Vulcan/pull/396)
- Use epic editor autogrow feature [`#395`](https://github.com/VulcanJS/Vulcan/pull/395)
- update epiceditor to latest(0.2.2) and unminified version [`#394`](https://github.com/VulcanJS/Vulcan/pull/394)
- use // instead of http:// for images [`#392`](https://github.com/VulcanJS/Vulcan/pull/392)
- fix email hash of gravatar [`#391`](https://github.com/VulcanJS/Vulcan/pull/391)
- uncommented and line 18 [`#384`](https://github.com/VulcanJS/Vulcan/pull/384)
- Fix header logo position [`#380`](https://github.com/VulcanJS/Vulcan/pull/380)
- Adds comments to API [`#378`](https://github.com/VulcanJS/Vulcan/pull/378)
- Remove unused signin [`#376`](https://github.com/VulcanJS/Vulcan/pull/376)
- update bootstrap datepicker [`#369`](https://github.com/VulcanJS/Vulcan/pull/369)
- Changed the default sign-in route from /signin to /sign-in [`#367`](https://github.com/VulcanJS/Vulcan/pull/367)
- Small customization enhancements and fix [`#351`](https://github.com/VulcanJS/Vulcan/pull/351)
- Don't iterate all the users for finding who to send notifications to. [`#338`](https://github.com/VulcanJS/Vulcan/pull/338)
- Fix digest date issues [`#321`](https://github.com/VulcanJS/Vulcan/pull/321)
- fixed - not showing user profiles [`#308`](https://github.com/VulcanJS/Vulcan/pull/308)
- [fix] undefined title in posts [`#305`](https://github.com/VulcanJS/Vulcan/pull/305)
- Fixed locale en [`#285`](https://github.com/VulcanJS/Vulcan/pull/285)
- Translations variable change to object [`#286`](https://github.com/VulcanJS/Vulcan/pull/286)
- fix for downvoting comments [`#278`](https://github.com/VulcanJS/Vulcan/pull/278)
- fixed typo related to profile picture Fetching [`#275`](https://github.com/VulcanJS/Vulcan/pull/275)
- Facebook integration [`#273`](https://github.com/VulcanJS/Vulcan/pull/273)
- Added Hack on Nitrous.IO button [`#271`](https://github.com/VulcanJS/Vulcan/pull/271)
- specify required versions of iron router and meteor in smart.json [`#268`](https://github.com/VulcanJS/Vulcan/pull/268)
- "New Posts" string for en.js was in French [`#264`](https://github.com/VulcanJS/Vulcan/pull/264)
- better redirection,error msg after post is deleted [`#263`](https://github.com/VulcanJS/Vulcan/pull/263)
- fixed issue for broken redirection to template after commment deletion [`#262`](https://github.com/VulcanJS/Vulcan/pull/262)
- Fixes #255: now canView do not wait for 'settingsLoaded' which was removed in e622c112 [`#256`](https://github.com/VulcanJS/Vulcan/pull/256)
- Use the outgoing click tracking for rss [`#250`](https://github.com/VulcanJS/Vulcan/pull/250)
- updated meteor to latest version [`#245`](https://github.com/VulcanJS/Vulcan/pull/245)
- categories are sorted by name [`#244`](https://github.com/VulcanJS/Vulcan/pull/244)
- Fix new post checkbox [`#242`](https://github.com/VulcanJS/Vulcan/pull/242)
- Only show category list on post submit/edit if there are categories [`#240`](https://github.com/VulcanJS/Vulcan/pull/240)
- Best practice to pass object than to check for optional parameter value [`#235`](https://github.com/VulcanJS/Vulcan/pull/235)
- added fast-render support [`#228`](https://github.com/VulcanJS/Vulcan/pull/228)
- #220: now all pages are waitOn('categories') [`#227`](https://github.com/VulcanJS/Vulcan/pull/227)
- #218: post_submit: <input name=category>s are now checkboxes, not radio [`#223`](https://github.com/VulcanJS/Vulcan/pull/223)
- router.js: all router-level access checks now wait for required subscriptions to be ready instead of hacking around [`#224`](https://github.com/VulcanJS/Vulcan/pull/224)
- #217: fixed bug with 'You have to be an admin' message displayed to admins [`#222`](https://github.com/VulcanJS/Vulcan/pull/222)
- #213: symlinks was removed from /packages/ - they should be locally created by Meteorite [`#221`](https://github.com/VulcanJS/Vulcan/pull/221)
- #194: fixed bug with preserving category name in posts after renaming [`#219`](https://github.com/VulcanJS/Vulcan/pull/219)
- #184: fixed subscription to Notifications collection [`#216`](https://github.com/VulcanJS/Vulcan/pull/216)
- User profile edit form: now it's prevented from submit and windows is scrolled to display error/success message [`#215`](https://github.com/VulcanJS/Vulcan/pull/215)
- For for #209: createNotifications() is a server-side function now [`#214`](https://github.com/VulcanJS/Vulcan/pull/214)
- Update from depreciated style events [`#212`](https://github.com/VulcanJS/Vulcan/pull/212)
- Spanish revised [`#205`](https://github.com/VulcanJS/Vulcan/pull/205)
- missing comma on line 178 [`#199`](https://github.com/VulcanJS/Vulcan/pull/199)
- add i18n chinese support [`#195`](https://github.com/VulcanJS/Vulcan/pull/195)
- Add Spanish i18n [`#198`](https://github.com/VulcanJS/Vulcan/pull/198)
- Update Events declaration style [`#188`](https://github.com/VulcanJS/Vulcan/pull/188)
- Nice search transition [`#187`](https://github.com/VulcanJS/Vulcan/pull/187)
- Use higher quality gravatar image [`#168`](https://github.com/VulcanJS/Vulcan/pull/168)
- Fix subscription for spiderable [`#167`](https://github.com/VulcanJS/Vulcan/pull/167)
- Use cursor to iterate lists of users [`#166`](https://github.com/VulcanJS/Vulcan/pull/166)
- Fix downvoting, cancelling upvoting & cancelling downvoting [`#164`](https://github.com/VulcanJS/Vulcan/pull/164)
- Show nothing instead of null [`#163`](https://github.com/VulcanJS/Vulcan/pull/163)
- Have no title if there isn't a title set instead of undefined [`#162`](https://github.com/VulcanJS/Vulcan/pull/162)
- Don't trust client ids [`#161`](https://github.com/VulcanJS/Vulcan/pull/161)
- Fix ability to delete posts [`#160`](https://github.com/VulcanJS/Vulcan/pull/160)
- Prevent weird deploy problem on some versions of node [`#155`](https://github.com/VulcanJS/Vulcan/pull/155)
- Check that people setting post.userId are actually admins before we set it [`#153`](https://github.com/VulcanJS/Vulcan/pull/153)
- Move analyticsRequest() [`#148`](https://github.com/VulcanJS/Vulcan/pull/148)
- delete post comments too when a post is deleted [`#136`](https://github.com/VulcanJS/Vulcan/pull/136)
- Added Error for Trying to Post Empty Comments [`#135`](https://github.com/VulcanJS/Vulcan/pull/135)
- Set document title to post headline [`#125`](https://github.com/VulcanJS/Vulcan/pull/125)
- Update epic-light.css to set a minimum height for the 'Message' text ent... [`#133`](https://github.com/VulcanJS/Vulcan/pull/133)
- Make upvote cleanup prior downvote and vice-versa [`#127`](https://github.com/VulcanJS/Vulcan/pull/127)
- Duplicate Template.post_submit.rendered assignment in post_submit.js [`#119`](https://github.com/VulcanJS/Vulcan/pull/119)
- update google+ share button style [`#123`](https://github.com/VulcanJS/Vulcan/pull/123)
- Added a template helper to address '1 points' [`#115`](https://github.com/VulcanJS/Vulcan/pull/115)
- Fix for nothing happening when editing another user [`#109`](https://github.com/VulcanJS/Vulcan/pull/109)
- 1 comments -> 1 comment [`#104`](https://github.com/VulcanJS/Vulcan/pull/104)
- fix avatar in user_profile page for oauth-login [`#98`](https://github.com/VulcanJS/Vulcan/pull/98)
- Adjust Logout button size for consistency [`#96`](https://github.com/VulcanJS/Vulcan/pull/96)
- Updating deny.update() and deny.remove() to v0.5.8 [`#95`](https://github.com/VulcanJS/Vulcan/pull/95)
- Add ability to pass 'limit' query string parameter [`#90`](https://github.com/VulcanJS/Vulcan/pull/90)
- Finally extracted database-forms into its own package [`#77`](https://github.com/VulcanJS/Vulcan/pull/77)
- Loading class is not removed if no url is provided on the Post page [`#66`](https://github.com/VulcanJS/Vulcan/pull/66)
- Commenting was broken [`#62`](https://github.com/VulcanJS/Vulcan/pull/62)
- Use generic getAvatarUrl instead of Gravatar. [`#54`](https://github.com/VulcanJS/Vulcan/pull/54)
- Add post link to mobile nav [`#47`](https://github.com/VulcanJS/Vulcan/pull/47)
- Update README.md [`#46`](https://github.com/VulcanJS/Vulcan/pull/46)
- Towards generic forms [`#31`](https://github.com/VulcanJS/Vulcan/pull/31)
- Various [`#9`](https://github.com/VulcanJS/Vulcan/pull/9)
- User karma [`#7`](https://github.com/VulcanJS/Vulcan/pull/7)
- Use this.userId() in publish rather than an arg. [`#6`](https://github.com/VulcanJS/Vulcan/pull/6)
- Meteor includes json2.js [`#5`](https://github.com/VulcanJS/Vulcan/pull/5)
- User profile pages [`#4`](https://github.com/VulcanJS/Vulcan/pull/4)
- Added rendered hooks (fix #330) [`#330`](https://github.com/VulcanJS/Vulcan/issues/330)
- fix #347 [`#347`](https://github.com/VulcanJS/Vulcan/issues/347)
- fix #320 [`#320`](https://github.com/VulcanJS/Vulcan/issues/320)
- fix #333 [`#333`](https://github.com/VulcanJS/Vulcan/issues/333)
- fix #331 [`#331`](https://github.com/VulcanJS/Vulcan/issues/331)
- fix #329 [`#329`](https://github.com/VulcanJS/Vulcan/issues/329)
- fix #327 [`#327`](https://github.com/VulcanJS/Vulcan/issues/327)
- smart.lock: versions of iron-router and fast-render were updated, fixes #259 (compatibility with Meteor 0.7.1) [`#259`](https://github.com/VulcanJS/Vulcan/issues/259)
- Merge pull request #256 from yeputons/issue-255 [`#255`](https://github.com/VulcanJS/Vulcan/issues/255)
- Fixes #255: now canView do not wait for 'settingsLoaded' which was removed in e622c112 [`#255`](https://github.com/VulcanJS/Vulcan/issues/255)
- fix #179 [`#179`](https://github.com/VulcanJS/Vulcan/issues/179)
- publish mailchimp data for admins (fix #176) [`#176`](https://github.com/VulcanJS/Vulcan/issues/176)
- Fix #159 [`#159`](https://github.com/VulcanJS/Vulcan/issues/159)
- Fixed #93 [`#93`](https://github.com/VulcanJS/Vulcan/issues/93)
- Separating themes; adding accounts-entry [`4ad0201`](https://github.com/VulcanJS/Vulcan/commit/4ad020174c76c7b0699ee1fb0dfb06dc3204d669)
- update epiceditor to latest and unminified version [`6de9c35`](https://github.com/VulcanJS/Vulcan/commit/6de9c35cb41168b4a438a50669b9ec02386548e7)
- add embedly and newsletter packages [`733f367`](https://github.com/VulcanJS/Vulcan/commit/733f367f37e81cca54ebe7d3d9a54e6e8755cd9c)
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at hello@vulcanjs.org. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
================================================
FILE: CONTRIBUTING.md
================================================
## Etiquette
- **All PRs should be made to the `devel` branch, not `master`.**
- Come check-in in the [Vulcan Slack channel](http://slack.telescopeapp.org/). 👋
- Completely new features should be shipped as external packages with their own repos (see [3rd party packages](https://docs.vulcanjs.org/plugins.html)). Don't hesitate to come by the [Slack channel](http://slack.telescopeapp.org/) to speak about it.
- ~~We don't have test at the moment, and Travis integration is broken. If you know how to fix it, you are welcome (see [#1253](https://github.com/TelescopeJS/Telescope/issues/1253)!~~ We are making progress on testing! Running `npm run test` will trigger client side and server side unit tests. Running `npm run test-client` or `npm run test-server` will run tests for a specific environnement. Using the `MOCHA_GREP` environment variable, you can run only tests matching some regular expression (eg `MOCHA_GREP="vulcan:core" npm run test-server`). Pull requests coming with automated tests will be greatly appreciated!
- Be nice 😉
## Branches
- `master` branch matches the latest version published on Atmosphere
- `devel` branch is the bleeding edge
- 1.X branch tracks a previous version of Vulcan (eg 1.13 may correspond to 1.13.2, 1.11 to 1.11.6, etc.). Those branches are only meant for publishing critical security fixes.
================================================
FILE: Dockerfile
================================================
FROM abernix/meteord:onbuild
================================================
FILE: MIGRATING.md
================================================
# Migrations
Doc to help updating downstream applications. Breaking changes and packages updates are listed here.
Please open an issue or a pull request if you feel this doc is incomplete.
## Updating Meteor
- Check that your version of `boilerplate-generator` is right. If not, overwrite it manually in `packages/_boilerplate/package.js`. This package is a hack to support SSR, so it's ok to manually change the version without actually updating
- Check that you don't have hard dependency on core packages, like `accounts-password@1.16.0`. They could conflict with Meteor core package version.
- Run `meteor update`. Note: when running the update on the Starter, remember to setup `METEOR_PACKAGES_DIRS=...` correctly, so it points to your local `devel` install of Vulcan.
## From 1.16 to 1.16.1
- `meteor update`
- `meteor npm i --save string-similarity @apollo/client`
- Migrate your code to Apollo client v3: https://www.apollographql.com/docs/react/migrating/apollo-client-3-migration/
- Migrate the names of base form controls in `vulcan:ui-material` if import them into your code. See `Vulcan/packages/vulcan-ui-material/history.md`.
## From 1.15 to 1.16
- `meteor npm i --save node-cache`
- Read Vulcan blog article related to 1.16
- Schemas without "_id" or "userId" won't have those fields in the default form fragment anymore (extremely edge case)
## From 1.14.1 to 1.15
- Update Meteor with `meteor update`
- /!\ Carefully update NPM packages versions based on the current package.json, otherwise install will fail
- `single2` hoc and hooks will return the whole `error` object, not just `error.graphQLErrors[0]`. This will help catching network errors too.
- Install `npm i --save body-parser-graphql`
- CORS are now disabled as a default in production. Use `apolloServer.corsWhitelist` to whitelist some domains, or `apolloServer.corsEnableAll` to allow all
connections.
## From 1.13.5 to 1.14
- See migration article from [Vulcan Blog](https://blog.vulcanjs.org/)
- `serverTimezoneOffset` object is no longer injected in the head during SSR. Use `import { InjectData} from 'meteor/vulcan:lib; ...; await InjectData.getData("utcOffset");` instead. The value is the reverse from `getTimezoneOffset`, see [Moment doc](https://momentjscom.readthedocs.io/en/latest/moment/03-manipulating/09-utc-offset/)
- `validateModifier` takes `data` as the second param (`validateModifier(modifier, data, document)` instead of `validateModifier(modifier, document)`)
### Material UI
- Update to v4 `meteor npm i --save-exact @material-ui/core@4.5.1`
- `import MuiThemeProvider from @material-ui/core/styles/MuiThemeProvider"` becomes `import { MuiThemeProvider } from "@material-ui/core/styles"`
- More broadly follow https://material-ui.com/guides/migration-v3/ to update Material UI to v4
- Follow the composition doc to handle `forwardRef` warnings: https://material-ui.com/guides/composition/#caveat-with-refs
## From 1.13.3 to 1.13.5
- `npm install apollo-utilities` (to run tests)
- Replace `Users.getViewableFields` by `Users.getReadableProjection`
## From 1.13.2 to 1.13.3
- Update React to a version over 16.8 (and under 17 which will bring breaking changes) to access hooks
- Update React Apollo and Apollo Client to access GraphQL hooks: `npm i --save-exact apollo-client@2.6.3; npm i --save react-apollo@3.0.0`
- `compose` is not exported by `react-apollo`, use `recompose` instead.
- More broadly see [`react-apollo` changelog](https://github.com/apollographql/react-apollo/blob/master/Changelog.md) for breaking changes
- `editMutation`, `newMutation` etc. are deprecated, use the new `updateFoo`, `createFoo` syntax. An error message is thrown where deprecated mutations are used to help debugging
- When using Vulcan data oriented hooks (`useMulti`, `useCreate`...), use the new `queryOptions` and `mutationOptions` option to pass options to the underlying `useQuery` and `useMutation` hooks.
Example call: `useMulti({collection: Foos, queryOptions: { errorPolicy: "all" } })`.
- No need to call `registerComponent` anymore to use Vulcan HOC. You can call them directly even if the underlying fragment is not yet registered.
- Watched Mutations has been removed because it didn't work anymore, in favour to better Apollo's `update` option for mutations.
================================================
FILE: README.md
================================================
# The repository is now archived.
Time has passed and the main team members have moved on to other projects. We're archiving the repository to make it clear that it will not receive further updates or fixes. You can find the evolution of Vulcan in Sacha and Eric's project [Devographics](https://github.com/Devographics/Monorepo)
[](#backers)
[](#sponsors)
# Vulcan
Vulcan is a React+GraphQL framework for Meteor.
[You might want to discover Vulcan Next](https://github.com/VulcanJS/vulcan-next), a port of Vulcan toward Next.js.
### Install
- [Full video tutorial](https://www.youtube.com/watch?v=aCjR9UrNqVk)
Install the latest version of Node and NPM. We recommend the usage of [NVM](http://nvm.sh).
You can then install [Meteor](https://www.meteor.com/install), which is used as the Vulcan build tool.
Clone the [Vulcan Starter repo](https://github.com/VulcanJS/Vulcan-Starter) locally.
Rename your `sample_settings.json` file to `settings.json`, then:
```sh
meteor npm install
meteor npm start
```
And open `http://localhost:3000/` in your browser.
Find more info in the [documentation](http://docs.vulcanjs.org/#Install).
### Links
- [Vulcan Homepage](http://vulcanjs.org)
- [Documentation](http://docs.vulcanjs.org)
- [Old Telescope Homepage](http://www.telescopeapp.org)
### Other Versions
[See all releases](https://github.com/VulcanJS/Vulcan/releases).
To update an existing Vulcan app, [see migration doc](MIGRATING.md)) and [changelog](CHANGELOG.md).
You can find the older, non-Apollo version of Telescope Nova on the [nova-classic](https://github.com/VulcanJS/Vulcan/tree/nova-classic) branch.
You can find the even older, non-React version of Telescope on the [legacy](https://github.com/VulcanJS/Vulcan/tree/legacy) branch.
## Credits
### Contributors
This project exists thanks to all the people who contribute.
### Backers
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/vulcan#contribute)]
### 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#contribute)]
================================================
FILE: RELEASE.md
================================================
# Release process
## Updating
We try to respect semantic versioning as much as possible. Going from 1.13.1 to 1.13.2 should cause almost no breaking changes, except for packages version update, small tweaks, etc. Going from 1.13 to 1.14 could cause multiple breaking changes. Going from 1.x to 2.x is a full rework.
Changes will be tracked in the changelog file.
### In Vulcan core repository
- If updating to a minor or major with non trivial breaking changes (1.13 to 1.14 for example), create a branch for the previous version based on master (1.13 in this example).
- Go to a `release/your-version` branch.
- Cleanup and reinstall everything
- Run unit tests, and apply relevant fixes
- Test that Storybook runs correctly
- Run tests, apply fixes if necessary
```sh
meteor reset && rm -Rf node_modules && meteor npm install
meteor npm run test
meteor npm run storybook
```
- Update packages versions in each package.
- Update package.json version.
- Update the CHANGELOG.md.
```sh
meteor npm run generate-changelog
```
- Merge release branch into `devel` (so that fixes from the release branch are shared) and then `master`.
- Go to `master` branch
- Create a tag for this version `git tag 1.x.x`.
- Push with `--tags` option: `git push && git push --tags`
- Deploy on Atmosphere
### In Vulcan-Starter
No need to maintain specific branches for versions, as the Starter is only meant to be used once for new projects initialization.
We only use `devel` and `master` branches.
- Go to `devel` branch.
- Update Vulcan packages versions in `.meteor/packages`.
- Check that the packages are working as expected, solve breaking changes.
- Check that `package.json` versions matches Vulcan's `package.json`.
- Cleanup and reinstall everything
- Run unit tests, and apply relevant fixes
- Test that Storybook runs correctly
```sh
meteor reset && rm -Rf node_modules && meteor npm install
METEOR_PACKAGE_DIRS="X/Vulcan/packages" meteor npm run test
METEOR_PACKAGE_DIRS="X/Vulcan/packages" meteor npm run storybook
```
- Test different example packages.
- Merge devel in to `master`.
- Update version `npm version patch` (for 1.16.1 > 1.16.2) or `npm version minor` for 1.16 > 1.17
- Push with `--tags`: `git push && git push --tags`.
### In the docs
- If updating to a minor or major with non trivial breaking changes (1.13 to 1.14 for example), create a branch for the previous version based on master (1.13 in this example).
- Do relevant updates on `devel` branch
- Merge into `master`
- Update the docs after packages are releved
================================================
FILE: jsconfig.json
================================================
{
"compilerOptions": {
"target": "ES6",
"baseUrl": ".",
"paths": {
"meteor/vulcan:styled-components": [
"packages/vulcan-styled-components/lib/server/main.js",
"packages/vulcan-styled-components/lib/client/main.js"
],
"meteor/vulcan:payments": [
"packages/vulcan-payments/lib/server/main.js",
"packages/vulcan-payments/lib/client/main.js"
],
"meteor/vulcan:events-intercom": [
"packages/vulcan-events-intercom/lib/client/main.js",
"packages/vulcan-events-intercom/lib/server/main.js"
],
"meteor/vulcan:embed": [
"packages/vulcan-embed/lib/client/main.js",
"packages/vulcan-embed/lib/server/main.js"
],
"meteor/boilerplate-generator": [
"packages/_boilerplate-generator/generator.js"
],
"meteor/vulcan:lib": [
"packages/vulcan-lib/lib/server/main.js",
"packages/vulcan-lib/lib/client/main.js"
],
"meteor/vulcan:forms": [
"packages/vulcan-forms/lib/client/main.js",
"packages/vulcan-forms/lib/server/main.js"
],
"meteor/vulcan:events": [
"packages/vulcan-events/lib/server/main.js",
"packages/vulcan-events/lib/client/main.js"
],
"meteor/vulcan:email": [
"packages/vulcan-email/lib/server.js",
"packages/vulcan-email/lib/client.js"
],
"meteor/meteortesting:mocha": [
"packages/meteor-mocha/client.js",
"packages/meteor-mocha/server.js"
],
"meteor/vulcan:admin": [
"packages/vulcan-admin/lib/server/main.js",
"packages/vulcan-admin/lib/client/main.js"
],
"meteor/vulcan:forms-tags": [
"packages/vulcan-forms-tags/lib/export.js"
],
"meteor/vulcan:ui-bootstrap": [
"packages/vulcan-ui-bootstrap/lib/server/main.js",
"packages/vulcan-ui-bootstrap/lib/client/main.js"
],
"meteor/vulcan:forms-upload": [
"packages/vulcan-forms-upload/lib/modules.js"
],
"meteor/vulcan:i18n": [
"packages/vulcan-i18n/lib/server/main.js",
"packages/vulcan-i18n/lib/client/main.js"
],
"meteor/vulcan:errors-sentry": [
"packages/vulcan-errors-sentry/lib/server/main.js",
"packages/vulcan-errors-sentry/lib/client/main.js"
],
"meteor/vulcan:errors": [
"packages/vulcan-errors/lib/server/main.js",
"packages/vulcan-errors/lib/client/main.js"
],
"meteor/vulcan:events-segment": [
"packages/vulcan-events-segment/lib/server/main.js",
"packages/vulcan-events-segment/lib/client/main.js"
],
"meteor/vulcan:ui-material": [
"packages/vulcan-ui-material/lib/client/main.js",
"packages/vulcan-ui-material/lib/server/main.js"
],
"meteor/vulcan:events-internal": [
"packages/vulcan-events-internal/lib/server/main.js",
"packages/vulcan-events-internal/lib/client/main.js"
],
"meteor/vulcan:events-ga": [
"packages/vulcan-events-ga/lib/server/main.js",
"packages/vulcan-events-ga/lib/client/main.js"
],
"meteor/vulcan:voting": [
"packages/vulcan-voting/lib/server/main.js",
"packages/vulcan-voting/lib/client/main.js"
],
"meteor/vulcan:newsletter": [
"packages/vulcan-newsletter/lib/server/main.js",
"packages/vulcan-newsletter/lib/client/main.js"
],
"meteor/vulcan:users": [
"packages/vulcan-users/lib/server/main.js",
"packages/vulcan-users/lib/client/main.js"
],
"meteor/vulcan:subscribe": [
"packages/vulcan-subscribe/lib/modules.js",
"packages/vulcan-subscribe/lib/modules.js"
],
"meteor/vulcan:core": [
"packages/vulcan-core/lib/server/main.js",
"packages/vulcan-core/lib/client/main.js"
],
"meteor/vulcan:test": [
"packages/vulcan-test/lib/server/main.js",
"packages/vulcan-test/lib/client/main.js"
],
"meteor/vulcan:redux": [
"packages/vulcan-redux/lib/server/main.js",
"packages/vulcan-redux/lib/client/main.js"
],
"meteor/vulcan:debug": [
"packages/vulcan-debug/lib/server/main.js",
"packages/vulcan-debug/lib/client/main.js"
],
"meteor/vulcan:cloudinary": [
"packages/vulcan-cloudinary/lib/client/main.js",
"packages/vulcan-cloudinary/lib/server/main.js"
],
"meteor/vulcan:accounts": [
"packages/vulcan-accounts/main_client.js",
"packages/vulcan-accounts/main_server.js"
]
}
},
"include": [
"packages/**/*"
],
"exclude": [
"packages/_buffer",
"packages/_boilerplate-generator"
],
"typeAcquisition": {
"enable": true
}
}
================================================
FILE: license.md
================================================
The MIT License (MIT)
Copyright (c) 2017 Telescope Nova
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: package.json
================================================
{
"name": "vulcan-meteor",
"version": "1.16.9",
"engines": {
"npm": "^3.0"
},
"scripts": {
"start": "meteor --settings settings.json",
"visualizer": "meteor --extra-packages bundle-visualizer --production --settings settings.json",
"lint": "eslint --cache --ext .jsx,js packages",
"lintfix": "eslint --cache --fix --ext .jsx,js packages",
"test-ci": "npm run test-server -- --once",
"test-unit": "meteor test-packages ./packages/* --port 60859 --driver-package meteortesting:mocha",
"open-test-client": "xdg-open http://localhost:60859",
"test-client": "TEST_SERVER=0 meteor test-packages ./packages/* --port 60859 --driver-package meteortesting:mocha",
"test-server": "TEST_CLIENT=0 meteor test-packages ./packages/* --port 60859 --driver-package meteortesting:mocha",
"test": "npm run test-unit",
"prettier": "node ./.vulcan/prettier/index.js write-changed",
"prettier-all": "node ./.vulcan/prettier/index.js write",
"update-package-json": "node ./.vulcan/update_package.js",
"storybook": "VULCAN_DIR=\"..\" start-storybook -p 6006",
"storybook-material": "STORYBOOK_UI=material VULCAN_DIR=\"..\" start-storybook -p 6006",
"build-storybook": "STORYBOOK_UI=material build-storybook -o docs/storybook-material && STORYBOOK_UI=bootstrap build-storybook -o docs/storybook-bootstrap",
"generate-changelog": "auto-changelog -u"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint"
}
},
"dependencies": {
"@apollo/client": "^3.2.5",
"@babel/runtime": "^7.13.9",
"analytics-node": "^2.1.1",
"apollo-cache": "^1.3.2",
"apollo-cache-inmemory": "1.3.12",
"apollo-client": "^2.6.4",
"apollo-errors": "^1.9.0",
"apollo-link": "^1.2.12",
"apollo-link-error": "^1.1.11",
"apollo-link-http": "^1.5.15",
"apollo-link-schema": "^1.2.3",
"apollo-link-state": "^0.4.2",
"apollo-server": "2.8.2",
"apollo-server-express": "2.8.2",
"apollo-utilities": "^1.3.2",
"bcrypt": "^5.0.0",
"body-parser": "^1.18.3",
"body-parser-graphql": "^1.1.0",
"chalk": "2.2.0",
"classnames": "^2.2.3",
"combined-stream2": "^1.1.2",
"compression": "^1.7.2",
"cookie-parser": "^1.4.3",
"cors": "^2.8.5",
"cross-fetch": "^0.0.8",
"crypto-js": "^3.1.9-1",
"crypto-random-string": "^3.3.0",
"dataloader": "^1.4.0",
"deepmerge": "^1.2.0",
"dot-object": "^1.7.0",
"enzyme-adapter-react-16": "^1.14.0",
"escape-string-regexp": "^1.0.5",
"express": "^4.17.1",
"flat": "^4.0.0",
"graphql": "14.4.2",
"graphql-anywhere": "4.1.13",
"graphql-date": "^1.0.3",
"graphql-tag": "^2.9.2",
"graphql-tools": "^4.0.4",
"graphql-type-json": "^0.1.4",
"graphql-voyager": "^1.0.0-rc.26",
"handlebars": "^4.4.3",
"he": "^1.1.1",
"history": "^3.0.0",
"html-to-text": "^2.1.0",
"immutability-helper": "^2.7.0",
"import": "0.0.6",
"intl": "^1.2.4",
"intl-locales-supported": "1.4.6",
"juice": "^5.1.0",
"lodash": "^4.17.19",
"mailchimp": "^1.1.6",
"marked": "^0.7.0",
"meteor-node-stubs": "^0.4.1",
"mingo": "^2.2.0",
"moment": "^2.13.0",
"node-cache": "^5.1.2",
"pluralize": "7.0.0",
"prop-types": "^15.7.2",
"qs": "^6.6.0",
"react": "16.12.0",
"react-addons-pure-render-mixin": "^15.4.1",
"react-bootstrap": "^1.0.0-beta.5",
"react-bootstrap-datetimepicker": "0.0.22",
"react-bootstrap-typeahead": "^4.2.0",
"react-cookie": "^4.0.3",
"react-datetime": "^2.11.1",
"react-dom": "16.12.0",
"react-dropzone": "11.0.1",
"react-helmet": "^6.0.0",
"react-intl": "^2.1.3",
"react-loadable": "^4.0.3",
"react-markdown": "^3.1.5",
"react-no-ssr": "^1.1.0",
"react-overlays": "^1.0.0-beta.17",
"react-places-autocomplete": "^5.0.0",
"react-redux": "^5.0.6",
"react-router": "^5.0.1",
"react-router-bootstrap": "0.25.0",
"react-router-dom": "^5.0.1",
"react-router-scroll": "^0.4.4",
"react-select": "^1.2.1",
"react-stripe-checkout": "^2.4.0",
"recompose": "^0.26.0",
"redux": "^3.6.0",
"rss": "^1.2.1",
"sanitize-html": "^1.16.3",
"simpl-schema": "^1.4.2",
"speakingurl": "^9.0.0",
"string-similarity": "^4.0.2",
"stripe": "^4.23.1",
"tracker-component": "^1.3.14",
"underscore": "^1.8.3",
"universal-cookie-express": "^2.1.5",
"url": "^0.11.0"
},
"private": true,
"devDependencies": {
"@apollo/react-testing": "3.1.4",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8",
"@babel/plugin-proposal-optional-chaining": "^7.13.8",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.57",
"@material-ui/styles": "4.10.0",
"@storybook/addon-actions": "5.0.8",
"@storybook/addon-knobs": "5.0.8",
"@storybook/addon-links": "5.0.1",
"@storybook/addons": "5.0.1",
"@storybook/react": "5.0.8",
"@storybook/theming": "5.0.8",
"@userfrosting/merge-package-dependencies": "^1.2.0",
"apollo-server-testing": "^2.10.1",
"auto-changelog": "^1.16.1",
"autoprefixer": "^6.3.6",
"autosize-input": "^1.0.2",
"autosuggest-highlight": "^3.1.1",
"babel-eslint": "^10.0.1",
"babel-runtime": "^6.26.0",
"babylon": "^6.18.0",
"chromedriver": "^2.46.0",
"colors": "^1.3.2",
"css-loader": "^2.1.1",
"diff": "^3.5.0",
"dompurify": "^2.2.6",
"enzyme": "^3.3.0",
"eslint": "^5.16.0",
"eslint-config-airbnb": "^17.1.0",
"eslint-config-meteor": "0.1.1",
"eslint-import-resolver-meteor": "^0.4.0",
"eslint-plugin-babel": "^5.3.0",
"eslint-plugin-import": "^2.16.00",
"eslint-plugin-jsx-a11y": "^6.2.1",
"eslint-plugin-meteor": "^5.1.0",
"eslint-plugin-mocha": "^5.3.0",
"eslint-plugin-prettier": "^3.0.1",
"eslint-plugin-react": "^7.12.4",
"expect": "^24.7.1",
"glob": "^7.1.3",
"husky": "^1.2.0",
"jsdom": "^11.11.0",
"jsdom-global": "^3.0.2",
"mdi-material-ui": "^6.16.0",
"moment-timezone": "^0.5.25",
"node-sass": "^4.14.0",
"operation-name-mock-link": "0.0.4",
"prettier": "^1.15.2",
"react-autosuggest": "^9.4.3",
"react-isolated-scroll": "^0.1.1",
"react-jss": "^8.6.1",
"react-keyboard-event-handler": "1.5.4",
"sass-loader": "^7.1.0",
"scrap-meteor-loader": "0.0.1",
"selenium-webdriver": "^3.6.0",
"sinon": "^6.3.5",
"storybook-addon-intl": "^2.4.1",
"storybook-react-router": "^1.0.5",
"supertest": "^4.0.2",
"vulcan-loader": "0.0.1",
"waait": "^1.0.5",
"webpack": "^4.31.0"
},
"postcss": {
"plugins": {
"autoprefixer": {
"browsers": [
"last 2 versions"
]
}
}
}
}
================================================
FILE: packages/.gitignore
================================================
/bootstrap3-datepicker
/npm-container
================================================
FILE: packages/_boilerplate-generator/.gitignore
================================================
.build*
================================================
FILE: packages/_boilerplate-generator/.npm/package/.gitignore
================================================
node_modules
================================================
FILE: packages/_boilerplate-generator/.npm/package/README
================================================
This directory and the files immediately inside it are automatically generated
when you change this package's NPM dependencies. Commit the files in this
directory (npm-shrinkwrap.json, .gitignore, and this README) to source control
so that others run the same versions of sub-dependencies.
You should NOT check in the node_modules directory that Meteor automatically
creates; if you are using git, the .gitignore file tells git to ignore it.
================================================
FILE: packages/_boilerplate-generator/.npm/package/npm-shrinkwrap.json
================================================
{
"lockfileVersion": 1,
"dependencies": {
"bluebird": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz",
"integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE="
},
"combined-stream2": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/combined-stream2/-/combined-stream2-1.1.2.tgz",
"integrity": "sha1-9uFLegFWZvjHsKH6xQYkAWSsNXA="
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"stream-length": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/stream-length/-/stream-length-1.0.2.tgz",
"integrity": "sha1-gnfzy+5JpNqrz9tOL0qbXp8snwA="
}
}
}
================================================
FILE: packages/_boilerplate-generator/README.md
================================================
# boilerplate-generator
[Source code of released version](https://github.com/meteor/meteor/tree/master/packages/boilerplate-generator) | [Source code of development version](https://github.com/meteor/meteor/tree/devel/packages/boilerplate-generator)
***
This is an internal Meteor package.
================================================
FILE: packages/_boilerplate-generator/generator.js
================================================
import { readFile } from 'fs';
import { create as createStream } from "combined-stream2";
import WebBrowserTemplate from './template-web.browser';
import WebCordovaTemplate from './template-web.cordova';
// Copied from webapp_server
const readUtf8FileSync = filename => Meteor.wrapAsync(readFile)(filename, 'utf8');
const identity = value => value;
function appendToStream(chunk, stream) {
if (typeof chunk === "string") {
stream.append(Buffer.from(chunk, "utf8"));
} else if (Buffer.isBuffer(chunk) ||
typeof chunk.read === "function") {
stream.append(chunk);
}
}
let shouldWarnAboutToHTMLDeprecation = ! Meteor.isProduction;
export class Boilerplate {
constructor(arch, manifest, options = {}) {
const { headTemplate, closeTemplate } = getTemplate(arch);
this.headTemplate = headTemplate;
this.closeTemplate = closeTemplate;
this.baseData = null;
this._generateBoilerplateFromManifest(
manifest,
options
);
}
toHTML(extraData) {
if (shouldWarnAboutToHTMLDeprecation) {
shouldWarnAboutToHTMLDeprecation = false;
console.error(
"The Boilerplate#toHTML method has been deprecated. " +
"Please use Boilerplate#toHTMLStream instead."
);
console.trace();
}
// Calling .await() requires a Fiber.
return this.toHTMLAsync(extraData).await();
}
// Returns a Promise that resolves to a string of HTML.
toHTMLAsync(extraData) {
return new Promise((resolve, reject) => {
const stream = this.toHTMLStream(extraData);
const chunks = [];
stream.on("data", chunk => chunks.push(chunk));
stream.on("end", () => {
resolve(Buffer.concat(chunks).toString("utf8"));
});
stream.on("error", reject);
});
}
// The 'extraData' argument can be used to extend 'self.baseData'. Its
// purpose is to allow you to specify data that you might not know at
// the time that you construct the Boilerplate object. (e.g. it is used
// by 'webapp' to specify data that is only known at request-time).
// this returns a stream
toHTMLStream(extraData) {
if (!this.baseData || !this.headTemplate || !this.closeTemplate) {
throw new Error('Boilerplate did not instantiate correctly.');
}
const data = {...this.baseData, ...extraData};
const start = "\n" + this.headTemplate(data);
const { body, dynamicBody } = data;
const end = this.closeTemplate(data);
const response = createStream();
appendToStream(start, response);
if (body) {
appendToStream(body, response);
}
if (dynamicBody) {
appendToStream(dynamicBody, response);
}
appendToStream(end, response);
return response;
}
// XXX Exported to allow client-side only changes to rebuild the boilerplate
// without requiring a full server restart.
// Produces an HTML string with given manifest and boilerplateSource.
// Optionally takes urlMapper in case urls from manifest need to be prefixed
// or rewritten.
// Optionally takes pathMapper for resolving relative file system paths.
// Optionally allows to override fields of the data context.
_generateBoilerplateFromManifest(manifest, {
urlMapper = identity,
pathMapper = identity,
baseDataExtension,
inline,
} = {}) {
const boilerplateBaseData = {
css: [],
js: [],
head: '',
body: '',
meteorManifest: JSON.stringify(manifest),
...baseDataExtension,
};
manifest.forEach(item => {
const urlPath = urlMapper(item.url);
const itemObj = { url: urlPath };
if (inline) {
itemObj.scriptContent = readUtf8FileSync(
pathMapper(item.path));
itemObj.inline = true;
}
if (item.type === 'css' && item.where === 'client') {
boilerplateBaseData.css.push(itemObj);
}
if (item.type === 'js' && item.where === 'client' &&
// Dynamic JS modules should not be loaded eagerly in the
// initial HTML of the app.
!item.path.startsWith('dynamic/')) {
boilerplateBaseData.js.push(itemObj);
}
if (item.type === 'head') {
boilerplateBaseData.head =
readUtf8FileSync(pathMapper(item.path));
}
if (item.type === 'body') {
boilerplateBaseData.body =
readUtf8FileSync(pathMapper(item.path));
}
});
this.baseData = boilerplateBaseData;
}
};
// Returns a template function that, when called, produces the boilerplate
// html as a string.
function getTemplate(arch) {
const prefix = arch.split(".", 2).join(".");
if (prefix === "web.browser") {
return WebBrowserTemplate;
}
if (prefix === "web.cordova") {
return WebCordovaTemplate;
}
throw new Error("Unsupported arch: " + arch);
}
================================================
FILE: packages/_boilerplate-generator/package.js
================================================
Package.describe({
summary: "Generates the boilerplate html from program's manifest",
version: '1.7.1',
name: 'boilerplate-generator-not-used',
});
Npm.depends({
'combined-stream2': '1.1.2',
});
Package.onUse(api => {
api.use('ecmascript');
api.use('underscore', 'server');
api.mainModule('generator.js', 'server');
api.export('Boilerplate', 'server');
});
================================================
FILE: packages/_boilerplate-generator/template-web.browser.js
================================================
import template from './template';
export const headTemplate = ({
css,
htmlAttributes,
bundledJsCssUrlRewriteHook,
head,
dynamicHead,
}) => {
var headSections = head.split(/]*>/, 2);
var cssBundle = [...(css || []).map(file =>
template(' ')({
href: bundledJsCssUrlRewriteHook(file.url),
})
)].join('\n');
return [
' template(' <%= attrName %>="<%- attrValue %>"')({
attrName: key,
attrValue: htmlAttributes[key],
})
).join('') + '>',
'',
dynamicHead,
(headSections.length === 1)
? [cssBundle, headSections[0]].join('\n')
: [headSections[0], cssBundle, headSections[1]].join('\n'),
'',
'',
].join('\n');
};
// Template function for rendering the boilerplate html for browsers
export const closeTemplate = ({
meteorRuntimeConfig,
rootUrlPathPrefix,
inlineScriptsAllowed,
js,
additionalStaticJs,
bundledJsCssUrlRewriteHook,
}) => [
'',
inlineScriptsAllowed
? template(' ')({
conf: meteorRuntimeConfig,
})
: template(' ')({
src: rootUrlPathPrefix,
}),
'',
...(js || []).map(file =>
template(' ')({
src: bundledJsCssUrlRewriteHook(file.url),
})
),
...(additionalStaticJs || []).map(({ contents, pathname }) => (
inlineScriptsAllowed
? template(' ')({
contents,
})
: template(' ')({
src: rootUrlPathPrefix + pathname,
})
)),
'',
'',
'',
''
].join('\n');
================================================
FILE: packages/_boilerplate-generator/template-web.cordova.js
================================================
import template from './template';
// Template function for rendering the boilerplate html for cordova
export const headTemplate = ({
meteorRuntimeConfig,
rootUrlPathPrefix,
inlineScriptsAllowed,
css,
js,
additionalStaticJs,
htmlAttributes,
bundledJsCssUrlRewriteHook,
head,
dynamicHead,
}) => {
var headSections = head.split(/]*>/, 2);
var cssBundle = [
// We are explicitly not using bundledJsCssUrlRewriteHook: in cordova we serve assets up directly from disk, so rewriting the URL does not make sense
...(css || []).map(file =>
template(' ')({
href: file.url,
})
)].join('\n');
return [
'',
'',
' ',
' ',
' ',
' ',
' ',
(headSections.length === 1)
? [cssBundle, headSections[0]].join('\n')
: [headSections[0], cssBundle, headSections[1]].join('\n'),
' ',
'',
' ',
...(js || []).map(file =>
template(' ')({
src: file.url,
})
),
...(additionalStaticJs || []).map(({ contents, pathname }) => (
inlineScriptsAllowed
? template(' ')({
contents,
})
: template(' ')({
src: rootUrlPathPrefix + pathname
})
)),
'',
head,
'',
'',
'',
].join('\n');
};
export function closeTemplate() {
return "\n";
}
================================================
FILE: packages/_boilerplate-generator/template.js
================================================
import { _ } from 'meteor/underscore';
// As identified in issue #9149, when an application overrides the default
// _.template settings using _.templateSettings, those new settings are
// used anywhere _.template is used, including within the
// boilerplate-generator. To handle this, _.template settings that have
// been verified to work are overridden here on each _.template call.
export default function template(text) {
return _.template(text, null, {
evaluate : /<%([\s\S]+?)%>/g,
interpolate : /<%=([\s\S]+?)%>/g,
escape : /<%-([\s\S]+?)%>/g,
});
};
================================================
FILE: packages/_buffer/buffer.js
================================================
global.Buffer = global.Buffer || require("buffer").Buffer;
================================================
FILE: packages/_buffer/package.js
================================================
Package.describe({
name: "buffer"
});
Package.onUse( function(api) {
api.use([
'ecmascript'
]);
api.addFiles([
'buffer.js'
], ['client']);
});
================================================
FILE: packages/meteor-mocha/browser-shim.js
================================================
/* eslint-disable */
/**
* Sourced from: https://github.com/nathanboktae/mocha-phantomjs-core
*/
(function () {
// A shim for non ES5 supporting browsers, like PhantomJS. Lovingly inspired by:
// http://www.angrycoding.com/2011/09/to-bind-or-not-to-bind-that-is-in.html
if (!('bind' in Function.prototype)) {
Function.prototype.bind = function () {
var funcObj = this;
var extraArgs = Array.prototype.slice.call(arguments);
var thisObj = extraArgs.shift();
return function () {
return funcObj.apply(thisObj, extraArgs.concat(Array.prototype.slice.call(arguments)));
};
};
}
function isFileReady(readyState) {
// Check to see if any of the ways a file can be ready are available as properties on the file's element
return (!readyState || readyState == 'loaded' || readyState == 'complete' || readyState == 'uninitialized');
}
function shimMochaProcess(M) {
// Mocha needs a process.stdout.write in order to change the cursor position.
M.process = M.process || {};
M.process.stdout = M.process.stdout || process.stdout;
M.process.stdout.write = function (s) { window.callPhantom({ stdout: s }); };
window.callPhantom({ getColWith: true });
}
function shimMochaInstance(m) {
var origRun = m.run, origUi = m.ui;
m.ui = function () {
var retval = origUi.apply(mocha, arguments);
window.callPhantom({ configureMocha: true });
m.reporter = function () { };
return retval;
};
m.run = function () {
window.callPhantom({ testRunStarted: m.suite.suites.length });
m.runner = origRun.apply(mocha, arguments);
if (m.runner.stats && m.runner.stats.end) {
window.callPhantom({ testRunEnded: m.runner });
} else {
m.runner.on('end', function () {
window.callPhantom({ testRunEnded: m.runner });
});
}
return m.runner;
};
}
Object.defineProperty(window, 'checkForMocha', {
value: function () {
var scriptTags = document.querySelectorAll('script'),
mochaScript = Array.prototype.filter.call(scriptTags, function (s) {
var src = s.getAttribute('src');
return src && src.match(/mocha\.js$/);
})[0];
if (mochaScript) {
mochaScript.onreadystatechange = mochaScript.onload = function () {
if (isFileReady(mochaScript.readyState)) {
initMochaPhantomJS();
}
};
}
}
});
Object.defineProperty(window, 'initMochaPhantomJS', {
value: function () {
shimMochaProcess(Mocha);
shimMochaInstance(mocha);
delete window.initMochaPhantomJS;
},
configurable: true
});
// Mocha needs the formating feature of console.log so copy node's format function and
// monkey-patch it into place. This code is copied from node's, links copyright applies.
// https://github.com/joyent/node/blob/master/lib/util.js
if (!console.format) {
console.format = function (f) {
if (typeof f !== 'string') {
return Array.prototype.map.call(arguments, function (arg) {
try {
return JSON.stringify(arg);
}
catch (_) {
return '[Circular]';
}
}).join(' ');
}
var i = 1;
var args = arguments;
var len = args.length;
var str = String(f).replace(/%[sdj%]/g, function (x) {
if (x === '%%') return '%';
if (i >= len) return x;
switch (x) {
case '%s': return String(args[i++]);
case '%d': return Number(args[i++]);
case '%j':
try {
return JSON.stringify(args[i++]);
} catch (_) {
return '[Circular]';
}
default:
return x;
}
});
for (var x = args[i]; i < len; x = args[++i]) {
if (x === null || typeof x !== 'object') {
str += ' ' + x;
} else {
str += ' ' + JSON.stringify(x);
}
}
return str;
};
var origError = console.error;
console.error = function () { origError.call(console, console.format.apply(console, arguments)); };
var origLog = console.log;
console.log = function () { origLog.call(console, console.format.apply(console, arguments)); };
}
})();
================================================
FILE: packages/meteor-mocha/client.js
================================================
/* eslint-disable no-console */
/* global Package: false */
import { mocha } from 'meteor/meteortesting:mocha-core';
import prepForHTMLReporter from './prepForHTMLReporter';
import './browser-shim';
let uncaughtExceptions = 0;
window.addEventListener('error', () => {
uncaughtExceptions++;
});
function saveCoverage(config, done) {
if (!config) {
done();
return;
}
if (typeof Package === 'undefined' || !Package.meteor || !Package.meteor.Meteor || !Package.meteor.Meteor.sendCoverage) {
console.error('Coverage package missing or not correctly launched');
done();
return;
}
Package.meteor.Meteor.sendCoverage((stats, err) => {
console.log('Meteor-coverage is saving client side coverage to the server. Client js files saved ', JSON.stringify(stats));
if (err) {
console.error('Failed to send client coverage');
}
done();
});
}
// Run the client tests. Meteor calls the `runTests` function exported by
// the driver package on the client.
function runTests() {
// We need to set the reporter when the tests actually run. This ensures that the
// correct reporter is used in the case where another Mocha test driver package is also
// added to the app. Since both are testOnly packages, top-level client code in both
// will run, potentially changing the reporter.
const { mochaOptions, runnerOptions, coverageOptions } = Meteor.settings.public.mochaRuntimeArgs || {};
if (!runnerOptions.runClient) return;
const { clientReporter, grep, invert, reporter } = mochaOptions || {};
if (grep) mocha.grep(grep);
if (invert) mocha.options.invert = invert;
// The chrome/webdriver logging adapter seems to escape color
// codes, so we can't support colors for that adapter.
// Feel free to fix this if you know how.
if (runnerOptions.browserDriver !== 'chrome') {
mocha.options.useColors = true;
}
let currentReporter = clientReporter || reporter;
if (!currentReporter) {
currentReporter = runnerOptions.browserDriver ? 'spec' : 'html';
}
if (currentReporter === 'html') {
// If we're not running client tests automatically in a headless browser, then we
// probably are going to want to see an HTML reporter when we load the page.
prepForHTMLReporter(mocha);
}
mocha.reporter(currentReporter);
// These `window` properties are all used by the client testing script in the
// browser-tests package to know what is happening.
window.testsAreRunning = true;
mocha.run((failures) => {
saveCoverage(coverageOptions, () => {
window.testsAreRunning = false;
window.testFailures = failures + uncaughtExceptions;
window.testsDone = true;
});
});
}
export { runTests };
================================================
FILE: packages/meteor-mocha/package.js
================================================
Package.describe({
name: 'meteortesting:mocha',
summary: 'Run Meteor package or app tests with Mocha',
git: 'https://github.com/meteortesting/meteor-mocha.git',
documentation: '../README.md',
version: '2.0.2',
testOnly: true,
});
Package.onUse(function onUse(api) {
api.use([
'meteortesting:mocha-core@8.1.2',
'ecmascript@0.3.0',
'lmieulet:meteor-coverage@4.0.0',
]);
api.use(['meteortesting:browser-tests@1.3.4', 'http@2.0.0'], 'server');
api.use('browser-policy@1.1.0', 'server', { weak: true });
api.mainModule('client.js', 'client');
api.mainModule('server.js', 'server');
});
================================================
FILE: packages/meteor-mocha/package.json
================================================
{
"name": "mocha",
"version": "0.0.0-semantic-release",
"repository": {
"type": "git",
"url": "https://github.com/meteortesting/meteor-mocha"
},
"author": "Dispatch Technologies, Inc. (http://www.dispatch.me/)",
"license": "MIT",
"release": {
"verifyConditions": ["semantic-release-meteor", "@semantic-release/github"],
"getLastRelease": "semantic-release-meteor",
"publish": ["semantic-release-meteor", "@semantic-release/github"]
}
}
================================================
FILE: packages/meteor-mocha/prepForHTMLReporter.js
================================================
export default function prepForHTMLReporter() {
// Add the CSS from CDN
const link = document.createElement('link');
link.setAttribute('rel', 'stylesheet');
link.setAttribute('href', 'https://cdn.rawgit.com/mochajs/mocha/2.2.5/mocha.css');
document.head.appendChild(link);
// Add the div#mocha in which test results HTML will be placed
const div = document.createElement('div');
div.setAttribute('id', 'mocha');
document.body.appendChild(div);
}
================================================
FILE: packages/meteor-mocha/runtimeArgs.js
================================================
export default function setArgs() {
const {
MOCHA_GREP,
MOCHA_INVERT,
MOCHA_REPORTER,
CLIENT_TEST_REPORTER,
SERVER_TEST_REPORTER,
TEST_BROWSER_DRIVER,
TEST_CLIENT,
TEST_PARALLEL,
TEST_SERVER,
TEST_WATCH,
METEOR_AUTO_RESTART, // Introduced in Meteor 1.8.1 to indicate if this instance will automatically restart after exiting. https://github.com/meteor/meteor/pull/10465
XUNIT_FILE,
SERVER_MOCHA_OUTPUT,
CLIENT_MOCHA_OUTPUT,
COVERAGE,
COVERAGE_VERBOSE,
COVERAGE_IN_COVERAGE,
COVERAGE_OUT_COVERAGE,
COVERAGE_OUT_LCOVONLY,
COVERAGE_OUT_HTML,
COVERAGE_OUT_JSON,
COVERAGE_OUT_JSON_SUMMARY,
COVERAGE_OUT_TEXT_SUMMARY,
COVERAGE_OUT_REMAP,
} = process.env;
const runtimeArgs = {
mochaOptions: {
grep: MOCHA_GREP || false,
invert: !!MOCHA_INVERT,
reporter: MOCHA_REPORTER,
serverReporter: SERVER_TEST_REPORTER || XUNIT_FILE, // XUNIT_FILE is left in here for compatibility to older versions
clientReporter: CLIENT_TEST_REPORTER,
serverOutput: SERVER_MOCHA_OUTPUT,
clientOutput: CLIENT_MOCHA_OUTPUT,
},
runnerOptions: {
runClient: (TEST_CLIENT !== 'false' && TEST_CLIENT !== '0'),
runServer: (TEST_SERVER !== 'false' && TEST_SERVER !== '0'),
browserDriver: TEST_BROWSER_DRIVER,
testWatch: TEST_WATCH || METEOR_AUTO_RESTART === 'true',
runParallel: !!TEST_PARALLEL,
},
};
if (COVERAGE === '1') {
runtimeArgs.coverageOptions = {
verbose: COVERAGE_VERBOSE === '1',
in: {
coverage: COVERAGE_IN_COVERAGE === 'true' || COVERAGE_IN_COVERAGE === '1',
},
out: {
coverage: COVERAGE_OUT_COVERAGE === 'true' || COVERAGE_OUT_COVERAGE === '1',
lcovonly: COVERAGE_OUT_LCOVONLY === 'true' || COVERAGE_OUT_LCOVONLY === '1',
html: COVERAGE_OUT_HTML === 'true' || COVERAGE_OUT_HTML === '1',
json: COVERAGE_OUT_JSON === 'true' || COVERAGE_OUT_JSON === '1',
json_summary: COVERAGE_OUT_JSON_SUMMARY === 'true' || COVERAGE_OUT_JSON_SUMMARY === '1',
text_summary: COVERAGE_OUT_TEXT_SUMMARY === 'true' || COVERAGE_OUT_TEXT_SUMMARY === '1',
remap: COVERAGE_OUT_REMAP === 'true' || COVERAGE_OUT_REMAP === '1',
},
};
}
// Set the variables for the client to access as well.
Meteor.settings.public = Meteor.settings.public || {};
Meteor.settings.public.mochaRuntimeArgs = runtimeArgs;
return runtimeArgs;
}
================================================
FILE: packages/meteor-mocha/server.handleCoverage.js
================================================
/* eslint-disable no-console */
import { HTTP } from 'meteor/http';
export default (coverageOptions) => {
let promise = Promise.resolve(true);
if (coverageOptions) {
const cLog = (...args) => {
if (coverageOptions.verbose) {
console.log(...args);
}
};
cLog('Export code coverage');
const importCoverageDump = () => new Promise((resolve, reject) => {
cLog('- In coverage');
HTTP.get(Meteor.absoluteUrl('coverage/import'), (error, response) => {
if (error) {
reject(new Error('Failed to import coverage file'));
return;
}
const { statusCode } = response;
if (statusCode !== 200) {
reject(new Error('Failed to import coverage file'));
}
resolve();
});
});
const exportReport = (fileType, reportType) => new Promise((resolve, reject) => {
cLog(`- Out ${fileType}`);
const url = Meteor.absoluteUrl(`/coverage/export/${fileType}`);
HTTP.get(url, (error, response) => {
if (error) {
reject(new Error(`Failed to save ${fileType} ${reportType}`));
return;
}
const { statusCode } = response;
if (statusCode !== 200) {
reject(new Error(`Failed to save ${fileType} ${reportType}`));
}
resolve();
});
});
const exportRemap = () => new Promise((resolve, reject) => {
cLog('- Out remap');
HTTP.get(Meteor.absoluteUrl('/coverage/export/remap'), (error, response) => {
if (error) {
reject(new Error('Failed to remap your coverage'));
return;
}
const { statusCode } = response;
if (statusCode !== 200) {
reject(new Error('Failed to remap your coverage'));
}
resolve();
});
});
if (coverageOptions.in.coverage) {
promise = promise.then(() => importCoverageDump());
}
if (coverageOptions.out.coverage) {
promise = promise.then(() => exportReport('coverage', 'dump'));
}
if (coverageOptions.out.lcovonly) {
promise = promise.then(() => exportReport('lcovonly', 'coverage'));
}
if (coverageOptions.out.html) {
promise = promise.then(() => exportReport('html', 'report'));
}
if (coverageOptions.out.json) {
promise = promise.then(() => exportReport('json', 'report'));
}
if (coverageOptions.out.text_summary) {
promise = promise.then(() => exportReport('text-summary', 'report'));
}
if (coverageOptions.out.remap) {
promise = promise.then(() => exportRemap());
}
if (coverageOptions.out.json_summary) {
promise = promise.then(() => exportReport('json-summary', 'dump'));
}
promise = promise.catch(console.error);
}
return promise;
};
================================================
FILE: packages/meteor-mocha/server.js
================================================
/* global Package */
/* eslint-disable no-console */
import { mochaInstance } from 'meteor/meteortesting:mocha-core';
import { startBrowser } from 'meteor/meteortesting:browser-tests';
import fs from 'fs';
import setArgs from './runtimeArgs';
import handleCoverage from './server.handleCoverage';
if (Package['browser-policy-common'] && Package['browser-policy-content']) {
const { BrowserPolicy } = Package['browser-policy-common'];
// Allow the remote mocha.css file to be inserted, in case any CSP stuff
// exists for the domain.
BrowserPolicy.content.allowInlineStyles();
BrowserPolicy.content.allowStyleOrigin('https://cdn.rawgit.com');
}
const { mochaOptions, runnerOptions, coverageOptions } = setArgs();
const { grep, invert, reporter, serverReporter, serverOutput, clientOutput } = mochaOptions || {};
// Since intermingling client and server log lines would be confusing,
// the idea here is to buffer all client logs until server tests have
// finished running and then dump the buffer to the screen and continue
// logging in real time after that if client tests are still running.
let serverTestsDone = false;
const clientLines = [];
function clientLogBuffer(line) {
if (serverTestsDone) {
// printing and removing the extra new-line character. The first was added by the client log, the second here.
console.log(line.replace(/\n$/, ''));
} else {
clientLines.push(line);
}
}
function printHeader(type) {
const lines = [
'\n--------------------------------',
Meteor.isAppTest ? `--- RUNNING APP ${type} TESTS ---` : `----- RUNNING ${type} TESTS -----`,
'--------------------------------\n',
];
lines.forEach((line) => {
if (type === 'CLIENT') {
clientLogBuffer(line);
} else {
console.log(line);
}
});
}
let callCount = 0;
let clientFailures = 0;
let serverFailures = 0;
function exitIfDone(type, failures) {
callCount++;
if (type === 'client') {
clientFailures = failures;
} else {
serverFailures = failures;
serverTestsDone = true;
clientLines.forEach((line) => {
// printing and removing the extra new-line character. The first was added by the client log, the second here.
console.log(line.replace(/\n$/, ''));
});
}
if (callCount === 2) {
// We only need to show this final summary if we ran both kinds of tests in the same console
if (runnerOptions.runServer && runnerOptions.runClient && runnerOptions.browserDriver) {
console.log('All tests finished!\n');
console.log('--------------------------------');
console.log(`${Meteor.isAppTest ? 'APP ' : ''}SERVER FAILURES: ${serverFailures}`);
console.log(`${Meteor.isAppTest ? 'APP ' : ''}CLIENT FAILURES: ${clientFailures}`);
console.log('--------------------------------');
}
handleCoverage(coverageOptions).then(() => {
// if no env for TEST_WATCH, tests should exit when done
if (!runnerOptions.testWatch) {
if (clientFailures + serverFailures > 0) {
process.exit(1); // exit with non-zero status if there were failures
} else {
process.exit(0);
}
}
});
}
}
function serverTests(cb) {
if (!runnerOptions.runServer) {
console.log('SKIPPING SERVER TESTS BECAUSE TEST_SERVER=0');
exitIfDone('server', 0);
if (cb) cb();
return;
}
printHeader('SERVER');
if (grep) mochaInstance.grep(grep);
if (invert) mochaInstance.options.invert = invert;
mochaInstance.options.useColors = true;
// We need to set the reporter when the tests actually run to ensure no conflicts with
// other test driver packages that may be added to the app but are not actually being
// used on this run.
mochaInstance.reporter(serverReporter || reporter || 'spec', {
output: serverOutput,
});
mochaInstance.run((failureCount) => {
if (typeof failureCount !== 'number') {
console.log('Mocha did not return a failure count for server tests as expected');
exitIfDone('server', 1);
} else {
exitIfDone('server', failureCount);
}
if (cb) cb();
});
}
function clientTests() {
if (!runnerOptions.runClient) {
console.log('SKIPPING CLIENT TESTS BECAUSE TEST_CLIENT=0');
exitIfDone('client', 0);
return;
}
if (!runnerOptions.browserDriver) {
console.log('Load the app in a browser to run client tests, or set the TEST_BROWSER_DRIVER environment variable. '
+ 'See https://github.com/meteortesting/meteor-mocha/blob/master/README.md#run-app-tests');
exitIfDone('client', 0);
return;
}
printHeader('CLIENT');
startBrowser({
stdout(data) {
if (clientOutput) {
fs.appendFileSync(clientOutput, data.toString());
} else {
clientLogBuffer(data.toString());
}
},
writebuffer(data) {
if (clientOutput) {
fs.appendFileSync(clientOutput, data.toString());
} else {
clientLogBuffer(data.toString());
}
},
stderr(data) {
if (clientOutput) {
fs.appendFileSync(clientOutput, data.toString());
} else {
clientLogBuffer(data.toString());
}
},
done(failureCount) {
if (typeof failureCount !== 'number') {
console.log('The browser driver package did not return a failure count for server tests as expected');
exitIfDone('client', 1);
} else {
exitIfDone('client', failureCount);
}
},
});
}
// Before Meteor calls the `start` function, app tests will be parsed and loaded by Mocha
function start() {
// Run in PARALLEL or SERIES
// Running in series is a better default since it avoids db and state conflicts for newbs.
// If you want parallel you will know these risks.
if (runnerOptions.runParallel) {
console.log('Warning: Running in parallel can cause side-effects from state/db sharing');
serverTests();
clientTests();
} else {
serverTests(() => {
clientTests();
});
}
}
export { start };
================================================
FILE: packages/vulcan-accounts/README.md
================================================
Vulcan accounts package (forked from https://github.com/studiointeract/accounts-ui)
================================================
FILE: packages/vulcan-accounts/imports/accounts_ui.js
================================================
import { Accounts } from 'meteor/accounts-base';
import { redirect } from './helpers.js';
/**
* @summary Accounts UI
* @namespace
* @memberOf Accounts
*/
Accounts.ui = {};
Accounts.ui._options = {
requestPermissions: [],
requestOfflineToken: {},
forceApprovalPrompt: {},
requireEmailVerification: false,
passwordSignupFields: 'USERNAME_AND_EMAIL',
minimumPasswordLength: 7,
loginPath: '/',
signUpPath: '/',
resetPasswordPath: null,
profilePath: '/',
changePasswordPath: null,
homeRoutePath: '/',
onSubmitHook: () => {},
onPreSignUpHook: () => new Promise(resolve => resolve()),
onPostSignUpHook: () => redirect(`${Accounts.ui._options.signUpPath}`),
onEnrollAccountHook: () => redirect(`${Accounts.ui._options.loginPath}`),
onResetPasswordHook: () => redirect(`${Accounts.ui._options.loginPath}`),
onVerifyEmailHook: () => redirect(`${Accounts.ui._options.profilePath}`),
onSignedInHook: () => redirect(`${Accounts.ui._options.homeRoutePath}`),
onSignedOutHook: () => redirect(`${Accounts.ui._options.homeRoutePath}`),
emailPattern: new RegExp('[^@]+@[^@\.]{2,}\.[^\.@]+'),
};
/**
* @summary Configure the behavior of [``](#react-accounts-ui).
* @anywhere
* @param {Object} options
* @param {Object} options.requestPermissions Which [permissions](#requestpermissions) to request from the user for each external service.
* @param {Object} options.requestOfflineToken To ask the user for permission to act on their behalf when offline, map the relevant external service to `true`. Currently only supported with Google. See [Meteor.loginWithExternalService](#meteor_loginwithexternalservice) for more details.
* @param {Object} options.forceApprovalPrompt If true, forces the user to approve the app's permissions, even if previously approved. Currently only supported with Google.
* @param {String} options.passwordSignupFields Which fields to display in the user creation form. One of '`USERNAME_AND_EMAIL`' (default), '`USERNAME_AND_OPTIONAL_EMAIL`', '`USERNAME_ONLY`', '`EMAIL_ONLY`'.
*/
Accounts.ui.config = function(options) {
// validate options keys
const VALID_KEYS = [
'passwordSignupFields',
'requestPermissions',
'requestOfflineToken',
'forbidClientAccountCreation',
'requireEmailVerification',
'minimumPasswordLength',
'loginPath',
'signUpPath',
'resetPasswordPath',
'profilePath',
'changePasswordPath',
'homeRoutePath',
'onSubmitHook',
'onPreSignUpHook',
'onPostSignUpHook',
'onEnrollAccountHook',
'onResetPasswordHook',
'onVerifyEmailHook',
'onSignedInHook',
'onSignedOutHook',
'validateField',
'emailPattern',
];
_.each(_.keys(options), function (key) {
if (!_.contains(VALID_KEYS, key))
throw new Error('Accounts.ui.config: Invalid key: ' + key);
});
// Deal with `passwordSignupFields`
if (options.passwordSignupFields) {
if (_.contains([
'USERNAME_AND_EMAIL',
'USERNAME_AND_OPTIONAL_EMAIL',
'USERNAME_ONLY',
'EMAIL_ONLY',
], options.passwordSignupFields)) {
Accounts.ui._options.passwordSignupFields = options.passwordSignupFields;
}
else {
throw new Error('Accounts.ui.config: Invalid option for `passwordSignupFields`: ' + options.passwordSignupFields);
}
}
// Deal with `requestPermissions`
if (options.requestPermissions) {
_.each(options.requestPermissions, function (scope, service) {
if (Accounts.ui._options.requestPermissions[service]) {
throw new Error('Accounts.ui.config: Can\'t set `requestPermissions` more than once for ' + service);
}
else if (!(scope instanceof Array)) {
throw new Error('Accounts.ui.config: Value for `requestPermissions` must be an array');
}
else {
Accounts.ui._options.requestPermissions[service] = scope;
}
});
}
// Deal with `requestOfflineToken`
if (options.requestOfflineToken) {
_.each(options.requestOfflineToken, function (value, service) {
if (service !== 'google')
throw new Error('Accounts.ui.config: `requestOfflineToken` only supported for Google login at the moment.');
if (Accounts.ui._options.requestOfflineToken[service]) {
throw new Error('Accounts.ui.config: Can\'t set `requestOfflineToken` more than once for ' + service);
}
else {
Accounts.ui._options.requestOfflineToken[service] = value;
}
});
}
// Deal with `forceApprovalPrompt`
if (options.forceApprovalPrompt) {
_.each(options.forceApprovalPrompt, function (value, service) {
if (service !== 'google')
throw new Error('Accounts.ui.config: `forceApprovalPrompt` only supported for Google login at the moment.');
if (Accounts.ui._options.forceApprovalPrompt[service]) {
throw new Error('Accounts.ui.config: Can\'t set `forceApprovalPrompt` more than once for ' + service);
}
else {
Accounts.ui._options.forceApprovalPrompt[service] = value;
}
});
}
// Deal with `requireEmailVerification`
if (options.requireEmailVerification) {
if (typeof options.requireEmailVerification != 'boolean') {
throw new Error('Accounts.ui.config: "requireEmailVerification" not a boolean');
}
else {
Accounts.ui._options.requireEmailVerification = options.requireEmailVerification;
}
}
// Deal with `minimumPasswordLength`
if (options.minimumPasswordLength) {
if (typeof options.minimumPasswordLength != 'number') {
throw new Error('Accounts.ui.config: "minimumPasswordLength" not a number');
}
else {
Accounts.ui._options.minimumPasswordLength = options.minimumPasswordLength;
}
}
// Deal with the hooks.
for (let hook of [
'onSubmitHook',
'onPreSignUpHook',
'onPostSignUpHook',
]) {
if (options[hook]) {
if (typeof options[hook] != 'function') {
throw new Error(`Accounts.ui.config: "${hook}" not a function`);
}
else {
Accounts.ui._options[hook] = options[hook];
}
}
}
// Deal with pattern.
for (let hook of [
'emailPattern',
]) {
if (options[hook]) {
if (!(options[hook] instanceof RegExp)) {
throw new Error(`Accounts.ui.config: "${hook}" not a Regular Expression`);
}
else {
Accounts.ui._options[hook] = options[hook];
}
}
}
// deal with the paths.
for (let path of [
'loginPath',
'signUpPath',
'resetPasswordPath',
'profilePath',
'changePasswordPath',
'homeRoutePath'
]) {
if (typeof options[path] !== 'undefined') {
if (options[path] !== null && typeof options[path] !== 'string') {
throw new Error(`Accounts.ui.config: ${path} is not a string or null`);
}
else {
Accounts.ui._options[path] = options[path];
}
}
}
// deal with redirect hooks.
for (let hook of [
'onEnrollAccountHook',
'onResetPasswordHook',
'onVerifyEmailHook',
'onSignedInHook',
'onSignedOutHook']) {
if (options[hook]) {
if (typeof options[hook] == 'function') {
Accounts.ui._options[hook] = options[hook];
}
else if (typeof options[hook] == 'string') {
Accounts.ui._options[hook] = () => redirect(options[hook]);
}
else {
throw new Error(`Accounts.ui.config: "${hook}" not a function or an absolute or relative path`);
}
}
}
};
export default Accounts;
================================================
FILE: packages/vulcan-accounts/imports/api/server/servicesListPublication.js
================================================
import { Meteor } from 'meteor/meteor';
import { getLoginServices } from '../../helpers.js';
Meteor.publish('servicesList', function() {
let services = getLoginServices();
if (Package['accounts-password']) {
services.push({name: 'password'});
}
let fields = {};
// Publish the existing services for a user, only name or nothing else.
services.forEach(service => fields[`services.${service.name}.name`] = 1);
return Meteor.users.find({ _id: this.userId }, { fields: fields});
});
================================================
FILE: packages/vulcan-accounts/imports/components.js
================================================
import './ui/components/Button.jsx';
import './ui/components/Buttons.jsx';
import './ui/components/Field.jsx';
import './ui/components/Fields.jsx';
import './ui/components/Form.jsx';
import './ui/components/FormMessage.jsx';
import './ui/components/FormMessages.jsx';
import './ui/components/StateSwitcher.jsx';
import './ui/components/LoginForm.jsx';
import './ui/components/LoginFormInner.jsx';
import './ui/components/PasswordOrService.jsx';
import './ui/components/SocialButtons.jsx';
import './ui/components/ResetPassword.jsx';
import './ui/components/EnrollAccount.jsx';
import './ui/components/VerifyEmail.jsx';
================================================
FILE: packages/vulcan-accounts/imports/emailTemplates.js
================================================
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') +
'>';
}
================================================
FILE: packages/vulcan-accounts/imports/helpers.js
================================================
import { Accounts } from 'meteor/accounts-base';
let browserHistory;
try {
browserHistory = require('react-router').browserHistory;
} catch(e) {
// swallow errors
}
export const loginButtonsSession = Accounts._loginButtonsSession;
export const STATES = {
SIGN_IN: Symbol.for('SIGN_IN'),
SIGN_UP: Symbol.for('SIGN_UP'),
PROFILE: Symbol.for('PROFILE'),
PASSWORD_CHANGE: Symbol.for('PASSWORD_CHANGE'),
PASSWORD_RESET: Symbol.for('PASSWORD_RESET'),
ENROLL_ACCOUNT: Symbol.for('ENROLL_ACCOUNT')
};
export function getLoginServices() {
// First look for OAuth services.
const services = Package['accounts-oauth'] ? Accounts.oauth.serviceNames() : [];
// Be equally kind to all login services. This also preserves
// backwards-compatibility.
services.sort();
return _.map(services, function(name){
return {name: name};
});
}
// Export getLoginServices using old style globals for accounts-base which
// requires it.
this.getLoginServices = getLoginServices;
export function hasPasswordService() {
// First look for OAuth services.
return !!Package['accounts-password'];
}
export function loginResultCallback(service, err) {
if (!err) {
// Do nothing
} else if (err instanceof Accounts.LoginCancelledError) {
// Do nothing
} else if (err instanceof ServiceConfiguration.ConfigError) {
// Do nothing
} else {
// loginButtonsSession.errorMessage(err.reason || "Unknown error");
}
if (Meteor.isClient) {
if (typeof redirect === 'string'){
window.location.href = '/';
}
if (typeof service === 'function'){
service();
}
}
}
export function passwordSignupFields() {
return Accounts.ui._options.passwordSignupFields || 'USERNAME_AND_EMAIL';
}
export function validateEmail(email, showMessage, clearMessage) {
if (passwordSignupFields() === 'USERNAME_AND_OPTIONAL_EMAIL' && email === '') {
return true;
}
if (Accounts.ui._options.emailPattern.test(email)) {
return true;
} else if (!email || email.length === 0) {
showMessage('accounts.error_email_required', 'warning', false, 'email');
return false;
} else {
showMessage('accounts.error_invalid_email', 'warning', false, 'email');
return false;
}
}
export function validatePassword(password = '', showMessage, clearMessage){
if (password.length >= Accounts.ui._options.minimumPasswordLength) {
return true;
} else {
const errMsg = 'accounts.error_minchar';
showMessage(errMsg, 'warning', false, 'password');
return false;
}
}
export function validateUsername(username, showMessage, clearMessage, formState) {
if ( username ) {
return true;
} else {
const fieldName = (passwordSignupFields() === 'USERNAME_ONLY' || formState === STATES.SIGN_UP) ? 'username' : 'usernameOrEmail';
showMessage('accounts.error_username_required', 'warning', false, fieldName);
return false;
}
}
export function redirect(redirect) {
if (Meteor.isClient) {
if (window.history) {
// Run after all app specific redirects, i.e. to the login screen.
Meteor.setTimeout(() => {
if (Package['kadira:flow-router']) {
Package['kadira:flow-router'].FlowRouter.go(redirect);
} else if (Package['kadira:flow-router-ssr']) {
Package['kadira:flow-router-ssr'].FlowRouter.go(redirect);
} else if (browserHistory) {
browserHistory.push(redirect);
} else {
window.history.pushState( {} , 'redirect', redirect );
}
}, 100);
}
}
}
export function capitalize(string) {
return string.replace(/\-/, ' ').split(' ').map(word => {
return word.charAt(0).toUpperCase() + word.slice(1);
}).join(' ');
}
================================================
FILE: packages/vulcan-accounts/imports/login_session.js
================================================
/* eslint-disable meteor/no-session */
import { Accounts } from 'meteor/accounts-base';
import { loginResultCallback, getLoginServices } from './helpers.js';
const VALID_KEYS = [
'dropdownVisible',
// XXX consider replacing these with one key that has an enum for values.
'inSignupFlow',
'inForgotPasswordFlow',
'inChangePasswordFlow',
'inMessageOnlyFlow',
'errorMessage',
'infoMessage',
// dialogs with messages (info and error)
'resetPasswordToken',
'enrollAccountToken',
'justVerifiedEmail',
'justResetPassword',
'configureLoginServiceDialogVisible',
'configureLoginServiceDialogServiceName',
'configureLoginServiceDialogSaveDisabled',
'configureOnDesktopVisible'
];
export const validateKey = function (key) {
if (!_.contains(VALID_KEYS, key))
throw new Error('Invalid key in loginButtonsSession: ' + key);
};
export const KEY_PREFIX = 'Meteor.loginButtons.';
// XXX This should probably be package scope rather than exported
// (there was even a comment to that effect here from before we had
// namespacing) but accounts-ui-viewer uses it, so leave it as is for
// now
Accounts._loginButtonsSession = {
set: function(key, value) {
validateKey(key);
if (_.contains(['errorMessage', 'infoMessage'], key))
throw new Error('Don\'t set errorMessage or infoMessage directly. Instead, use errorMessage() or infoMessage().');
this._set(key, value);
},
_set: function(key, value) {
Session.set(KEY_PREFIX + key, value);
},
get: function(key) {
validateKey(key);
return Session.get(KEY_PREFIX + key);
}
};
if (Meteor.isClient){
// In the login redirect flow, we'll have the result of the login
// attempt at page load time when we're redirected back to the
// application. Register a callback to update the UI (i.e. to close
// the dialog on a successful login or display the error on a failed
// login).
//
Accounts.onPageLoadLogin(function (attemptInfo) {
// Ignore if we have a left over login attempt for a service that is no longer registered.
if (_.contains(_.pluck(getLoginServices(), 'name'), attemptInfo.type))
loginResultCallback(attemptInfo.type, attemptInfo.error);
});
// let doneCallback;
Accounts.onResetPasswordLink(function (token, done) {
Accounts._loginButtonsSession.set('resetPasswordToken', token);
Session.set(KEY_PREFIX + 'state', 'resetPasswordToken');
// doneCallback = done;
Accounts.ui._options.onResetPasswordHook();
});
Accounts.onEnrollmentLink(function (token, done) {
Accounts._loginButtonsSession.set('enrollAccountToken', token);
Session.set(KEY_PREFIX + 'state', 'enrollAccountToken');
// doneCallback = done;
Accounts.ui._options.onEnrollAccountHook();
});
Accounts.onEmailVerificationLink(function (token, done) {
Accounts.verifyEmail(token, function (error) {
if (! error) {
Accounts._loginButtonsSession.set('justVerifiedEmail', true);
Session.set(KEY_PREFIX + 'state', 'justVerifiedEmail');
Accounts.ui._options.onSignedInHook();
}
else {
Accounts.ui._options.onVerifyEmailHook();
}
done();
});
});
}
================================================
FILE: packages/vulcan-accounts/imports/oauth_config.js
================================================
import { ServiceConfiguration } from 'meteor/service-configuration';
import { getSetting } from 'meteor/vulcan:lib';
const services = getSetting('oAuth');
if (services) {
Object.keys(services).forEach(serviceName => {
ServiceConfiguration.configurations.upsert({service: serviceName}, {
$set: services[serviceName]
});
});
}
================================================
FILE: packages/vulcan-accounts/imports/routes.js
================================================
import { addRoute } from 'meteor/vulcan:core';
addRoute({name: 'resetPassword', path: '/reset-password/:token', componentName: 'AccountsResetPassword'});
addRoute({name: 'enrollAccount', path: '/enroll-account/:token', componentName: 'AccountsEnrollAccount'});
addRoute({name: 'verifyEmail', path: '/verify-email/:token', componentName: 'AccountsVerifyEmail'});
================================================
FILE: packages/vulcan-accounts/imports/ui/components/Button.jsx
================================================
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { Components, registerComponent } from 'meteor/vulcan:core';
export class AccountsButton extends PureComponent {
render () {
const {
label,
// href = null,
type,
disabled = false,
id,
className,
onClick
} = this.props;
return type === 'link' ?
{label}
:
{label}
;
}
}
AccountsButton.propTypes = {
onClick: PropTypes.func
};
registerComponent('AccountsButton', AccountsButton);
================================================
FILE: packages/vulcan-accounts/imports/ui/components/Buttons.jsx
================================================
import React from 'react';
import './Button.jsx';
import { Components, registerComponent } from 'meteor/vulcan:core';
export class Buttons extends React.Component {
render () {
let { buttons = {}, className = 'buttons' } = this.props;
return (
{Object.keys(buttons).map((id, i) =>
)}
);
}
}
registerComponent('AccountsButtons', Buttons);
================================================
FILE: packages/vulcan-accounts/imports/ui/components/EnrollAccount.jsx
================================================
import { Components, registerComponent, withCurrentUser } from 'meteor/vulcan:core';
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router';
import { intlShape } from 'meteor/vulcan:i18n';
import { STATES } from '../../helpers.js';
class AccountsEnrollAccount extends PureComponent {
componentDidMount() {
const token = this.props.match.params.token;
Accounts._loginButtonsSession.set('enrollAccountToken', token);
}
render() {
if (!this.props.currentUser) {
return (
);
} else {
return (
);
};
MenuItem.propTypes = {
...menuItemProps,
// parent can pass another onClick callback
// eg to close the menu
afterClick: PropTypes.func,
};
const Layout = ({ children, currentUser }) => {
const [open, setOpen] = useState(true);
const backofficeMenuItems = getAuthorizedMenuItems(currentUser, 'vulcan-backoffice');
const side = ;
return (
{
setOpen(!open);
}}
basePath={'/backoffice'}
/>
);
};
registerComponent({
name: 'VulcanBackofficeLayout',
component: Layout,
hocs: [withRouter, withCurrentUser],
});
================================================
FILE: packages/vulcan-backoffice/lib/components/CollectionItem.jsx
================================================
/**
* Generic page for a collection element
*
* Must be handled by the parent :
* - the document, using withDocument and options
*/
import React from 'react';
import { registerComponent, Components, withCurrentUser, withAccess } from 'meteor/vulcan:core';
// TODO: get options from backoffice config
const accessOptions = {
groups: ['admins'],
redirect: '/backoffice',
message: 'Sorry, you do not have the rights to access this page.',
};
const CollectionItemDetails = props => {
if (props.loading) return ;
if (!props.document) return 'Document not found';
return ;
};
registerComponent({
name: 'VulcanBackofficeCollectionItem',
component: CollectionItemDetails,
hocs: [withCurrentUser, [withAccess, accessOptions]],
});
================================================
FILE: packages/vulcan-backoffice/lib/components/CollectionList.jsx
================================================
/**
* Generic page for a collection
* Must be handled by the parent :
* - providing the documents and callbacks
*/
import React from 'react';
import { Components, registerComponent, withCurrentUser, withAccess } from 'meteor/vulcan:core';
// TODO: get options from backoffice config
const accessOptions = {
groups: ['admins'],
redirect: '/backoffice',
message: 'Sorry, you do not have the rights to access this page.',
};
export const CollectionList = props => {
return ;
};
export default CollectionList;
registerComponent({
name: 'VulcanBackofficeCollectionList',
component: CollectionList,
hocs: [withCurrentUser, [withAccess, accessOptions]],
});
================================================
FILE: packages/vulcan-backoffice/lib/hocs/withDocumentId.js
================================================
/**
* Get the documentId from parent props or from the route
*/
import React from 'react';
export const withDocumentId = (fieldName = 'documentId') => Component => {
const withDocumentId = props => (
);
withDocumentId.displayName = `withDocumentId(${Component.displayName})`;
return withDocumentId;
};
export default withDocumentId;
================================================
FILE: packages/vulcan-backoffice/lib/hocs/withRouteParam.js
================================================
/**
* Pass a route param to its child
*
*/
import React from 'react';
import PropTypes from 'prop-types';
export const withRouteParam = fieldName => Component => {
const Wrapper = props => (
);
Wrapper.propTypes = {
// @see React router 4 withRouter API
match: PropTypes.shape({
params: PropTypes.object,
}),
};
Wrapper.displayName = `withRouteParam(${fieldName})(${Component.displayName})`;
return Wrapper;
};
export default withRouteParam;
================================================
FILE: packages/vulcan-backoffice/lib/modules/components.js
================================================
// load generic components
import '../components/CollectionItem';
import '../components/CollectionList';
import '../components/BackofficeLayout';
import '../components/BackofficeIndex';
//import '../components/BackofficeVerticalMenuLayout';
================================================
FILE: packages/vulcan-backoffice/lib/modules/createCollectionComponents/createCollectionComponents.js
================================================
/**
* Create List and Item components for the provided collection,
* based on the generic Vulcan backoffice components
*/
import createListComponent from './createListComponent';
import createItemComponent from './createItemComponent';
import { mergeDefaultCollectionOptions } from '../options';
const createCollectionComponents = (collection, options) => {
const mergedOptions = mergeDefaultCollectionOptions(options);
const ListComponent = createListComponent(collection, mergedOptions);
const ItemComponent = createItemComponent(collection, mergedOptions);
return { ListComponent, ItemComponent };
};
export default createCollectionComponents;
================================================
FILE: packages/vulcan-backoffice/lib/modules/createCollectionComponents/createItemComponent.js
================================================
import React, { PureComponent } from 'react';
import { registerComponent, Components, withSingle, withAccess } from 'meteor/vulcan:core';
import { getItemComponentName } from '../namingHelpers';
import { withRouteParam } from '../../hocs/withRouteParam';
/**
* Create the item details page
*/
const createItemComponent = (collection, options) => {
const componentName = getItemComponentName(collection);
const component = class DetailsComponent extends PureComponent {
render() {
const { loading, document } = this.props;
return (
);
}
};
component.displayName = componentName;
const withDocumentOptions = {
collection,
};
const withAccessOptions = {
groups: options.item.accessGroups,
redirect: options.item.accessRedirect,
};
registerComponent({
name: componentName,
component: component,
hocs: [
[withAccess, withAccessOptions],
withRouteParam('documentId'),
[withSingle, withDocumentOptions],
],
});
return component; // return if the component is needed
};
export default createItemComponent;
================================================
FILE: packages/vulcan-backoffice/lib/modules/createCollectionComponents/createListComponent.js
================================================
import React, { PureComponent } from 'react';
import { registerComponent, Components, withAccess } from 'meteor/vulcan:core';
import { getListComponentName } from '../namingHelpers';
const createListComponent = (collection, options) => {
const component = class ListComponent extends PureComponent {
render() {
const { ...otherProps } = this.props;
const { list } = options;
const { ...otherListOptions } = list;
return (
);
}
};
const withAccessOptions = {
groups: options.list.accessGroups,
redirect: options.list.accessRedirect,
};
const componentName = getListComponentName(collection);
component.displayName = componentName;
registerComponent({
name: componentName,
component: component,
hocs: [[withAccess, withAccessOptions]],
});
return component;
};
export default createListComponent;
================================================
FILE: packages/vulcan-backoffice/lib/modules/createCollectionComponents/index.js
================================================
export { default as createListComponent } from './createListComponent';
export { default as createItemComponent } from './createItemComponent';
export { default } from './createCollectionComponents';
================================================
FILE: packages/vulcan-backoffice/lib/modules/index.js
================================================
import './settings';
import './startup';
import './components';
export { default as withDocumentId } from '../hocs/withDocumentId';
export { default as createCollectionComponents } from './createCollectionComponents';
export * from './setupCollectionMenuItems';
export { default as setupCollectionRoutes } from './setupCollectionRoutes';
export { default, default as setupBackoffice } from './setupBackoffice';
================================================
FILE: packages/vulcan-backoffice/lib/modules/namingHelpers.js
================================================
const capitalizeFirstLetter = string => string.charAt(0).toUpperCase() + string.slice(1);
export const getCollectionName = collection => {
return collection.options.collectionName;
};
export const getCollectionDisplayName = collection =>
capitalizeFirstLetter(getCollectionName(collection));
const makeComponentName = suffix => collection =>
`VulcanBackoffice${capitalizeFirstLetter(getCollectionName(collection))}${suffix}`;
export const getItemComponentName = makeComponentName('Item');
export const getListComponentName = makeComponentName('List');
export const getFormComponentName = makeComponentName('Form');
export const getFragmentName = makeComponentName('DefaultFragment');
export const getBaseRouteName = collection => getCollectionName(collection).toLowerCase();
// return {basePath}/collection-name
export const getBasePath = (collection, basePath) => {
const collectionBasePath = '/' + getBaseRouteName(collection);
return typeof basePath !== 'undefined' ? basePath + collectionBasePath : collectionBasePath;
};
export const getNewPath = (collection, basePath) => getBasePath(collection, basePath) + '/create';
export const getEditPath = (collection, basePath) =>
getBasePath(collection, basePath) + '/:documentId/edit';
export const getDetailsPath = (collection, basePath) =>
getBasePath(collection, basePath) + '/:documentId';
================================================
FILE: packages/vulcan-backoffice/lib/modules/options.js
================================================
/**
* Setup default options and provides helper to generate valid options based
* on these defaults
*/
import _merge from 'lodash/merge';
export const devOptions = {
list: { accessGroups: ['guests', 'members', 'admins'] },
item: { accessGroups: ['guests', 'members', 'admins'] },
menuItem: { groups: ['guests', 'members', 'admins'] },
layoutName: 'VulcanBackofficeLayout'
};
const defaultCollectionOptions = {
list: { accessGroups: ['admins'], accessRedirect: '/' },
item: { accessGroups: ['admins'], accessRedirect: '/' },
menuItem: {
groups: ['admins'],
},
layoutName: 'VulcanBackofficeLayout',
};
const defaultBackofficeOptions = {
//generateUI: true,
basePath: '/backoffice',
...defaultCollectionOptions,
};
export const mergeDefaultCollectionOptions = (collectionOptions, options = {}) =>
_merge({}, defaultBackofficeOptions, options, collectionOptions);
export const mergeDefaultBackofficeOptions = options =>
_merge({}, defaultBackofficeOptions, options);
================================================
FILE: packages/vulcan-backoffice/lib/modules/settings.js
================================================
import { registerSetting } from 'meteor/vulcan:core';
registerSetting('backoffice.enable', Meteor.isDevelopment, 'Automatically generate a backoffice', true);
================================================
FILE: packages/vulcan-backoffice/lib/modules/setupBackoffice.js
================================================
/** Setup a full fledged backoffice
* - create components
* - create routes
* - register menu items
*/
import { addRoute } from 'meteor/vulcan:core';
import { mergeDefaultBackofficeOptions, mergeDefaultCollectionOptions } from './options';
import { getCollectionName } from './namingHelpers';
import createCollectionComponents from './createCollectionComponents';
import setupCollectionRoutes from './setupCollectionRoutes';
import setupCollectionMenuItems from './setupCollectionMenuItems';
const setupBackoffice = (collections, providedOptions = {}, collectionsOptions = {}) => {
const options = mergeDefaultBackofficeOptions(providedOptions);
// pages for each collection
collections.forEach(collection => {
const collectionName = getCollectionName(collection);
const collectionOptions = mergeDefaultCollectionOptions(
collectionsOptions[collectionName],
options
);
const { ListComponent, ItemComponent } = createCollectionComponents(
collection,
collectionOptions
);
setupCollectionRoutes(collection, collectionOptions);
setupCollectionMenuItems(collection, collectionOptions);
});
// index
addRoute({
name: 'vulcan-backoffice', path: options.basePath,
componentName: 'VulcanBackofficeIndex',
layoutName: 'VulcanBackofficeLayout',
options
}); // setup the route
};
export default setupBackoffice;
================================================
FILE: packages/vulcan-backoffice/lib/modules/setupCollectionMenuItems.js
================================================
/** Add an item to the menu to access the collection */
import { addMenuItem, getMenuItems, getAuthorizedMenuItems } from 'meteor/vulcan:core';
import { getBasePath, getCollectionName, getCollectionDisplayName } from './namingHelpers';
import { mergeDefaultCollectionOptions } from './options';
const adminMenuName = 'vulcan-backoffice';
export const setupCollectionMenuItems = (collection, collectionOptions) => {
const options = mergeDefaultCollectionOptions(collectionOptions);
const labelToken = options.menuItem.labelToken;
const label = !labelToken
? options.menuItem.label || getCollectionDisplayName(collection)
: undefined;
const collectionName = getCollectionName(collection);
addMenuItem({
name: collectionName,
label,
labelToken: labelToken,
path: options.menuItem.basePath || getBasePath(collection, options.basePath),
groups: options.menuItem.groups,
menuGroup: adminMenuName,
});
};
// to retrieve the items
export const getBackofficeMenuItems = () => getMenuItems(adminMenuName);
export const getAuthorizedBackofficeMenuItems = currentUser =>
getAuthorizedMenuItems(currentUser, adminMenuName);
export default setupCollectionMenuItems;
================================================
FILE: packages/vulcan-backoffice/lib/modules/setupCollectionRoutes.js
================================================
import { addRoute } from 'meteor/vulcan:core';
import {
getBasePath,
getBaseRouteName,
getDetailsPath,
getListComponentName,
getItemComponentName,
} from './namingHelpers';
import { mergeDefaultCollectionOptions } from './options';
import _values from 'lodash/values';
export const generateRoutes = (collection, options) => {
const basePath = getBasePath(collection, options.basePath);
const detailsPath = getDetailsPath(collection, options.basePath);
const baseRouteName = getBaseRouteName(collection);
const routes = {
listRoute: {
name: 'vulcan-backoffice-' + baseRouteName,
path: basePath,
componentName: getListComponentName(collection),
returnRoute: basePath,
layoutName: options.layoutName,
},
itemRoute: {
name: 'vulcan-backoffice-' + baseRouteName + '-details',
path: detailsPath,
componentName: getItemComponentName(collection),
returnRoute: basePath,
layoutName: options.layoutName,
},
};
return routes;
};
export default (collection, providedOptions = {}) => {
const options = mergeDefaultCollectionOptions(providedOptions);
const routes = generateRoutes(collection, options);
_values(routes).forEach(route => {
addRoute(route);
});
return routes;
};
================================================
FILE: packages/vulcan-backoffice/lib/modules/startup.js
================================================
/**
* Generate the backoffice on startup
*/
import {getSetting, Collections} from 'meteor/vulcan:core';
import setupBackoffice from './setupBackoffice';
import {devOptions} from './options';
import {addCallback} from 'meteor/vulcan:lib';
const enabled = getSetting('backoffice.enabled', Meteor.isDevelopment);
if (enabled) {
const options = Meteor.isDevelopment ? devOptions : undefined; // loose permissions during development
// setupBackoffice must be run before routes and components are populated
// but after startup so that Collections are available
addCallback('populate.before', function _setupBackoffice() {
setupBackoffice(Collections, options);
});
}
================================================
FILE: packages/vulcan-backoffice/lib/server/main.js
================================================
export * from '../modules';
================================================
FILE: packages/vulcan-backoffice/package.js
================================================
Package.describe({
name: 'vulcan:backoffice',
summary: 'Vulcan automated backoffice generator',
version: '1.16.9',
git: 'https://github.com/VulcanJS/Vulcan.git',
});
Package.onUse(api => {
api.use(['vulcan:core@=1.16.9', 'vulcan:i18n@=1.16.9', 'vulcan:accounts@1.16.9']);
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.mainModule('./test/index.js');
});
================================================
FILE: packages/vulcan-backoffice/package.json
================================================
{
"name": "vulcan-backoffice",
"version": "0.0.1",
"description": "",
"main": "package.js",
"directories": {
"lib": "lib"
},
"scripts": {
"test": "npm run test-unit",
"test-unit": "TEST_WATCH=1 meteor test-packages ./ --port 3002 --driver-package meteortesting:mocha --raw-logs"
},
"devDependencies": {
"expect": "^23.6.0"
}
}
================================================
FILE: packages/vulcan-backoffice/test/index.js
================================================
import './namingHelpers.test';
import './routes.test';
================================================
FILE: packages/vulcan-backoffice/test/namingHelpers.test.js
================================================
import expect from 'expect';
import {
getCollectionName,
getBasePath,
getNewPath,
getEditPath,
getDetailsPath,
} from '../lib/modules/namingHelpers';
const dummyCollectionName = 'Dummies';
const DummyCollection = {
options: {
collectionName: dummyCollectionName,
},
};
describe('vulcan:backoffice/namingHelpers', function () {
it('get collection name', function () {
const collectionName = getCollectionName(DummyCollection);
expect(collectionName).toEqual(dummyCollectionName);
});
it('get base path', function () {
const basePath = getBasePath(DummyCollection);
expect(basePath).toEqual('/dummies');
});
it('add a prefix to base path', function () {
const basePath = getBasePath(DummyCollection, '/admin');
expect(basePath).toEqual('/admin/dummies');
});
it('get new path', function () {
const Path = getNewPath(DummyCollection);
expect(Path).toEqual('/dummies/create');
});
it('get edit path', function () {
const Path = getEditPath(DummyCollection);
expect(Path).toEqual('/dummies/:documentId/edit');
});
it('get details path', function () {
const Path = getDetailsPath(DummyCollection);
expect(Path).toEqual('/dummies/:documentId');
});
});
================================================
FILE: packages/vulcan-backoffice/test/options.js
================================================
import expect from 'expect';
import {
mergeDefaultBackofficeOptions,
mergeDefaultCollectionOptions,
} from '../lib/modules/options';
describe('options', function () {
it('merge defaultOptions', function () {
const givenOptions = {
menuItem: { groups: ['members', 'admins', 'foobars'] },
};
const mergedOptions = mergeDefaultBackofficeOptions(givenOptions);
expect(mergedOptions.menuItem.groups).toEqual(['members', 'admins', 'foobars']);
});
});
================================================
FILE: packages/vulcan-backoffice/test/routes.test.js
================================================
import expect from 'expect';
import { generateRoutes } from '../lib/modules/setupCollectionRoutes';
const DummyCollection = {
options: {
collectionName: 'Dummies',
},
};
describe('vulcan:backoffice/setupCollectionRoutes', function () {
it('generate routes', function () {
const options = {}
const { listRoute, itemRoute } = generateRoutes(DummyCollection, options);
//expect(baseRoute.path).toEqual('/dummies');
expect(listRoute.path).toEqual('/dummies');
expect(itemRoute.path).toEqual('/dummies/:documentId');
});
});
================================================
FILE: packages/vulcan-cloudinary/README.md
================================================
Vulcan file upload package, used internally.
### Custom Posts Fields
- `cloudinaryId`
- `cloudinaryUrls`
### Public Settings
- `cloudinaryCloudName`
- `cloudinaryFormats`
### Private Settings
- `cloudinaryAPIKey`
- `cloudinaryAPISecret`
### Sample Settings
```js
{
"public": {
"cloudinaryCloudName": "myCloudName",
"cloudinaryFormats": [
{
"name": "small",
"width": 120,
"height": 90
},
{
"name": "medium",
"width": 480,
"height": 360
}
]
},
"cloudinaryAPIKey": "abcfoo",
"cloudinaryAPISecret": "123bar",
}
```
================================================
FILE: packages/vulcan-cloudinary/lib/client/main.js
================================================
export * from '../modules/index.js';
export * from './make_cloudinary.js';
================================================
FILE: packages/vulcan-cloudinary/lib/client/make_cloudinary.js
================================================
import { addCustomFields } from '../modules/index.js';
export const makeCloudinary = ({collection, fieldName}) => {
addCustomFields(collection);
};
================================================
FILE: packages/vulcan-cloudinary/lib/modules/custom_fields.js
================================================
export const CloudinaryCollections = [];
export const addCustomFields = collection => {
CloudinaryCollections.push(collection);
collection.addField([
{
fieldName: 'cloudinaryId',
fieldSchema: {
type: String,
optional: true,
canRead: ['guests'],
}
},
{
fieldName: 'cloudinaryUrls',
fieldSchema: {
type: Array,
optional: true,
canRead: ['guests'],
}
},
{
fieldName: 'cloudinaryUrls.$',
fieldSchema: {
type: Object,
blackbox: true,
optional: true
}
},
// GraphQL only
{
fieldName: 'cloudinaryUrl',
fieldSchema: {
type: String,
optional: true,
canRead: ['guests'],
resolveAs: {
type: 'String',
arguments: 'format: String',
resolver: (document, {format}, context) => {
const image = format ? _.findWhere(document.cloudinaryUrls, {name: format}) : document.cloudinaryUrls[0];
return image && image.url;
}
},
}
},
]);
};
================================================
FILE: packages/vulcan-cloudinary/lib/modules/index.js
================================================
export * from './custom_fields.js';
================================================
FILE: packages/vulcan-cloudinary/lib/server/cloudinary.js
================================================
import cloudinary from 'cloudinary';
import { Utils, getSetting, registerSetting } from 'meteor/vulcan:core';
registerSetting('cloudinary', null, 'Cloudinary settings');
export const Cloudinary = cloudinary.v2;
const uploadSync = Meteor.wrapAsync(Cloudinary.uploader.upload);
const cloudinarySettings = getSetting('cloudinary');
Cloudinary.config({
cloud_name: cloudinarySettings.cloudName,
api_key: cloudinarySettings.apiKey,
api_secret: cloudinarySettings.apiSecret,
secure: true,
});
export const CloudinaryUtils = {
// send an image URL to Cloudinary and get a cloudinary result object in return
uploadImage(imageUrl) {
try {
var result = uploadSync(Utils.addHttp(imageUrl));
const data = {
cloudinaryId: result.public_id,
result: result,
urls: CloudinaryUtils.getUrls(result.public_id)
};
return data;
} catch (error) {
console.log("// Cloudinary upload failed for URL: "+imageUrl); // eslint-disable-line
console.log(error); // eslint-disable-line
}
},
// generate signed URL for each format based off public_id
getUrls(cloudinaryId) {
return cloudinarySettings.formats.map(format => {
const url = Cloudinary.url(cloudinaryId, {
width: format.width,
height: format.height,
crop: 'fill',
sign_url: true,
fetch_format: 'auto',
quality: 'auto'
});
return {
name: format.name,
url: url
};
});
}
};
// methods
// Meteor.methods({
// testCloudinaryUpload: function (thumbnailUrl) {
// if (Users.isAdmin(Meteor.user())) {
// thumbnailUrl = typeof thumbnailUrl === "undefined" ? "http://www.telescopeapp.org/images/logo.png" : thumbnailUrl;
// const data = CloudinaryUtils.uploadImage(thumbnailUrl);
// console.log(data); // eslint-disable-line
// }
// },
// cachePostThumbnails: function (limit = 20) {
// if (Users.isAdmin(Meteor.user())) {
// console.log(`// caching ${limit} thumbnails…`)
// var postsWithUncachedThumbnails = Posts.find({
// thumbnailUrl: { $exists: true },
// originalThumbnailUrl: { $exists: false }
// }, {sort: {createdAt: -1}, limit: limit});
// postsWithUncachedThumbnails.forEach(Meteor.bindEnvironment((post, index) => {
// Meteor.setTimeout(function () {
// console.log(`// ${index}. Caching thumbnail for post “${post.title}” (_id: ${post._id})`); // eslint-disable-line
// const data = CloudinaryUtils.uploadImage(post.thumbnailUrl);
// Posts.update(post._id, {$set:{
// cloudinaryId: data.cloudinaryId,
// cloudinaryUrls: data.urls
// }});
// }, index * 1000);
// }));
// }
// }
// });
================================================
FILE: packages/vulcan-cloudinary/lib/server/main.js
================================================
export * from './cloudinary.js';
export * from '../modules/index.js';
export * from './make_cloudinary.js';
================================================
FILE: packages/vulcan-cloudinary/lib/server/make_cloudinary.js
================================================
import { CloudinaryUtils } from '../server/cloudinary.js';
import { getSetting, addCallback } from 'meteor/vulcan:core';
import { addCustomFields } from '../modules/index.js';
const cloudinarySettings = getSetting('cloudinary');
export const CloudinaryCollections = [];
export const makeCloudinary = ({collection, fieldName}) => {
addCustomFields(collection);
// post submit callback
function cacheImageOnNew (document) {
if (cloudinarySettings) {
if (document[fieldName]) {
const data = CloudinaryUtils.uploadImage(document[fieldName]);
if (data) {
document.cloudinaryId = data.cloudinaryId;
document.cloudinaryUrls = data.urls;
}
}
}
return document;
}
addCallback(`${collection.options.collectionName.toLowerCase()}.new.sync`, cacheImageOnNew);
function cacheImageOnEdit (modifier, oldDocument) {
if (cloudinarySettings) {
if (modifier.$set[fieldName] && modifier.$set[fieldName] !== oldDocument[fieldName]) {
const data = CloudinaryUtils.uploadImage(modifier.$set[fieldName]);
modifier.$set.cloudinaryId = data.cloudinaryId;
modifier.$set.cloudinaryUrls = data.urls;
}
}
return modifier;
}
addCallback(`${collection.options.collectionName.toLowerCase()}.edit.sync`, cacheImageOnEdit);
};
================================================
FILE: packages/vulcan-cloudinary/package.js
================================================
Package.describe({
name: 'vulcan:cloudinary',
summary: 'Vulcan file upload package.',
version: '1.16.9',
git: 'https://github.com/VulcanJS/Vulcan.git',
});
Package.onUse(function(api) {
api.use(['vulcan:core@=1.16.9']);
api.mainModule('lib/client/main.js', 'client');
api.mainModule('lib/server/main.js', 'server');
});
================================================
FILE: packages/vulcan-core/README.md
================================================
Vulcan core package, used internally.
================================================
FILE: packages/vulcan-core/lib/client/components/AppGenerator.jsx
================================================
/**
* The App + relevant wrappers
*/
import React from 'react';
import { ApolloProvider } from '@apollo/client';
import { runCallbacks } from '../../modules';
import { Components } from 'meteor/vulcan:lib';
import { CookiesProvider } from 'react-cookie';
import { BrowserRouter } from 'react-router-dom';
const AppGenerator = ({ apolloClient }) => {
const App = (
);
// run user registered callbacks to wrap the app
const WrappedApp = runCallbacks({
name: 'router.client.wrapper',
iterator: App,
properties: { apolloClient }
});
return WrappedApp;
};
export default AppGenerator;
================================================
FILE: packages/vulcan-core/lib/client/main.js
================================================
export * from '../modules/index.js';
export * from './start.jsx';
================================================
FILE: packages/vulcan-core/lib/client/start.jsx
================================================
import React from 'react';
import ReactDOM from 'react-dom';
import { onPageLoad } from 'meteor/server-render';
import AppGenerator from './components/AppGenerator';
import { InjectData } from 'meteor/vulcan:lib';
import {
createApolloClient,
populateComponentsApp,
populateRoutesApp,
initializeFragments,
getSetting,
runCallbacks,
} from 'meteor/vulcan:lib';
const disableSsr = getSetting('apolloSsr.disable', false);
Meteor.startup(() => {
// run functions that must be called before populating components or routes
runCallbacks('populate.before');
// init the application components and routes, including components & routes from 3rd-party packages
initializeFragments();
populateComponentsApp();
populateRoutesApp();
const apolloClient = createApolloClient();
// Create the root element
const rootElement = document.createElement('div');
rootElement.id = 'react-app';
document.body.appendChild(rootElement);
const Main = () => ;
if (!disableSsr) {
onPageLoad(() => {
const ssrUrl = InjectData.getDataSync('url');
// in localhost hostname is null
if (ssrUrl && ssrUrl.hostname && ssrUrl.hostname !== window.location.hostname) {
console.warn(
`Mismatch between the browser hostname (${
window.location.hostname
}) and the hostname used during SSR (${
ssrUrl.hostname
}). Will prevent full rehydration of the React DOM.`
);
} else {
ReactDOM.hydrate(, document.getElementById('react-app'));
}
});
} else {
ReactDOM.render(, document.getElementById('react-app'));
}
});
================================================
FILE: packages/vulcan-core/lib/modules/callbacks.js
================================================
import { registerCallback } from 'meteor/vulcan:lib';
registerCallback({
name: 'populate.before',
description: 'Run before Vulcan objects are populated. Use if you need to add routes dynamically on startup for example.',
arguments: [],
runs: 'sync',
returns: 'nothing',
});
================================================
FILE: packages/vulcan-core/lib/modules/components/AccessControl.jsx
================================================
import React from 'react';
import { Components, registerComponent } from 'meteor/vulcan:lib';
import { useCurrentUser } from '../containers/currentUser';
import Users from 'meteor/vulcan:users';
import { useHistory } from 'react-router-dom';
import { withMessages } from '../containers/withMessages';
import { intlShape } from 'meteor/vulcan:i18n';
const AccessControl = ({ currentRoute, children, flash }, { intl }) => {
const { loading, currentUser } = useCurrentUser();
const { access } = currentRoute;
const history = useHistory();
if (!access) {
return children;
}
const { groups, redirect, redirectMessage, check } = access;
if (loading) {
return ;
} else if (!currentUser) {
if (redirect) {
history.push(redirect);
flash(
redirectMessage ? redirectMessage : intl.formatMessage({ id: 'app.please_sign_up_log_in', defaultMessage: 'Please log in first.' })
);
return null;
} else {
return ;
}
} else {
const canAccess = check ? check(currentUser, currentRoute) : groups ? Users.isMemberOf(currentUser, groups) : true;
return canAccess ? children : ;
}
};
AccessControl.displayName = 'AccessControl';
AccessControl.contextTypes = {
intl: intlShape,
};
registerComponent({ name: 'AccessControl', component: AccessControl, hocs: [withMessages] });
const FailureComponent = props => {
const { failureComponentName, failureComponent, ...rest } = props;
if (failureComponentName) {
const FailureComponent = Components[failureComponentName];
return ;
} else if (failureComponent) {
const FailureComponent = failureComponent; // necesary because jsx components must be uppercase
return ;
} else return ;
};
const DefaultLogInFailureComponent = () => (
);
registerComponent({ name: 'DefaultLogInFailureComponent', component: DefaultLogInFailureComponent });
const DefaultPermissionFailureComponent = () => (
);
registerComponent({ name: 'DefaultPermissionFailureComponent', component: DefaultPermissionFailureComponent });
export default AccessControl;
================================================
FILE: packages/vulcan-core/lib/modules/components/App.jsx
================================================
import { Components, registerComponent, Strings, runCallbacks, hasIntlFields, Routes, getLocale, getStrings } from 'meteor/vulcan:lib';
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { IntlProvider, intlShape, IntlContext } from 'meteor/vulcan:i18n';
import withCurrentUser from '../containers/currentUser.js';
import withUpdate from '../containers/update.js';
import withSiteData from '../containers/siteData.js';
import { withLocaleData, withLocales } from '../containers/localeData.js';
import { withApollo } from '@apollo/client/react/hoc';
import { withCookies } from 'react-cookie';
import moment from 'moment';
import { Switch, Route } from 'react-router-dom';
import { withRouter } from 'react-router';
import get from 'lodash/get';
import merge from 'lodash/merge';
import { SSRProvider } from '@react-aria/ssr';
// see https://stackoverflow.com/questions/42862028/react-router-v4-with-multiple-layouts
const RouteWithLayout = ({ layoutComponent, layoutName, component, currentRoute, ...rest }) => {
// if defined, use ErrorCatcher component to wrap layout contents
const ErrorCatcher = Components.ErrorCatcher ? Components.ErrorCatcher : Components.Dummy;
return (
RouteWithLayout > Route
// (instead of just Switch > Route), we must write
//exact
{...rest}
render={props => {
const layoutProps = { ...props, currentRoute };
const childComponentProps = { ...props, currentRoute };
// Use layoutComponent, or else registered layout component; or else default layout
const layout = layoutComponent ? layoutComponent : layoutName ? Components[layoutName] : Components.Layout;
const children = (
{React.createElement(component, childComponentProps)}
);
return React.createElement(layout, layoutProps, children);
}}
/>
);
};
class App extends PureComponent {
constructor(props) {
super(props);
const { currentUser, locale } = props;
if (currentUser) {
runCallbacks('events.identify', currentUser);
}
// get translation strings loaded dynamically
const loadedStrings = get(props.locale, 'data.locale.strings');
// get translation strings bundled statically
const bundledStrings = Strings[locale.id];
this.state = {
locale: {
id: locale.id,
rtl: locale.rtl ?? false,
method: locale.method,
loading: false,
strings: merge({}, loadedStrings, bundledStrings),
},
};
moment.locale(locale.id);
}
componentDidMount = async () => {
runCallbacks('app.mounted', this.props);
};
// actually returns an id, not a locale
getLocale = () => {
return this.state.locale.id;
};
setLocale = async localeId => {
// note: this is the getLocale in intl.js, not this.getLocale()!
const localeObject = getLocale(localeId);
const { cookies, updateUser, client, currentUser } = this.props;
let localeStrings;
// if this is a dynamic locale, fetch its data from the server
if (localeObject.dynamic) {
this.setState({ locale: { ...this.state.locale, loading: true, rtl: localeObject?.rtl ?? false } });
localeStrings = await this.loadLocaleStrings(localeId);
} else {
localeStrings = getStrings(localeId);
}
// before removing the loading we have to change the rtl class on HTML tag if it exists
if (document && typeof document.getElementsByTagName === 'function' && document.getElementsByTagName('html')) {
const htmlTag = document.getElementsByTagName('html');
if (htmlTag && htmlTag.length === 1) {
// change in locale didn't change the html lang as well, which is fixed by this PR
htmlTag[0].lang = localeId;
if (localeObject?.rtl === true) {
htmlTag[0].classList.add('rtl');
} else {
htmlTag[0].classList.remove('rtl');
}
}
}
this.setState({
locale: { ...this.state.locale, loading: false, id: localeId, rtl: localeObject?.rtl ?? false, strings: localeStrings },
});
cookies.remove('locale', { path: '/' });
cookies.set('locale', localeId, { path: '/' });
// if user is logged in, change their `locale` profile property
if (currentUser) {
await updateUser({
selector: { documentId: currentUser._id },
data: { locale: localeId },
});
}
moment.locale(localeId);
if (hasIntlFields) {
client.resetStore();
}
};
/*
Load a locale by triggering the refetch() method passed down by
withLocalData HoC
*/
loadLocaleStrings = async localeId => {
const result = await this.props.locale.refetch({ localeId });
const fetchedLocaleStrings = get(result, 'data.locale.strings', []);
const localeStrings = merge({}, this.state.localeStrings, fetchedLocaleStrings);
return localeStrings;
};
getChildContext() {
return {
getLocale: this.getLocale,
setLocale: this.setLocale,
};
}
componentDidUpdate(nextProps) {
const currentUser = this.props.currentUser;
const nextUser = nextProps.currentUser;
if (nextUser && (!currentUser || currentUser._id !== nextUser._id)) {
runCallbacks('events.identify', nextUser);
}
}
render() {
const routeNames = Object.keys(Routes);
const localeId = this.state.locale.id;
//const LayoutComponent = currentRoute.layoutName ? Components[currentRoute.layoutName] : Components.Layout;
const intlObject = {
locale: localeId,
key: localeId,
messages: this.state.locale.strings,
};
// keep IntlProvider for now for backwards compatibility with legacy Context API
return (
{this.props.currentUserLoading ? (
) : routeNames.length ? (
{routeNames.map(key => (
// NOTE: if we want the exact props to be taken into account
// we have to pass it to the RouteWithLayout, not the underlying Route,
// because it is the direct child of Switch
))}
{/* */}
) : (
)}
{this.state.locale.loading && (
);
registerComponent({ name: 'DatatableCellLayout', component: DatatableCellLayout, hocs: [memo] });
/*
DatatableDefaultCell Component
*/
const DatatableDefaultCell = ({ column, document, ...rest }) => (
);
registerComponent({ name: 'DatatableDefaultCell', component: DatatableDefaultCell, hocs: [memo] });
================================================
FILE: packages/vulcan-core/lib/modules/components/Datatable/DatatableContents.jsx
================================================
import { Components, registerComponent } from 'meteor/vulcan:lib';
import React, { memo } from 'react';
import PropTypes from 'prop-types';
import _sortBy from 'lodash/sortBy';
const wrapColumns = c => ({ name: c });
const getColumns = (columns, results, data) => {
if (columns) {
// convert all columns to objects
const convertedColums = columns.map(column => (typeof column === 'object' ? column : { name: column }));
const sortedColumns = _sortBy(convertedColums, column => column.order);
return sortedColumns;
} else if (results && results.length > 0) {
// if no columns are provided, default to using keys of first array item
return Object.keys(results[0])
.filter(k => k !== '__typename')
.map(wrapColumns);
} else if (data) {
// note: withMulti HoC also passes a prop named data, but in this case
// data should be the prop passed to the Datatable
return Object.keys(data[0]).map(wrapColumns);
}
return [];
};
/*
DatatableContents Component
*/
const DatatableContents = props => {
let {
title,
collection,
datatableData,
results = [],
columns,
loading,
loadMore,
count,
totalCount,
networkStatus,
showEdit,
showDelete,
currentUser,
toggleSort,
currentSort,
submitFilters,
currentFilters,
modalProps,
Components,
error,
showSelect,
} = props;
if (loading) {
return (
);
}
const isLoadingMore = networkStatus === 2;
const hasMore = results && totalCount > results.length;
const sortedColumns = getColumns(columns, results, datatableData);
return (
{/* note: we want to be able to show potential errors while still showing the data below */}
{error && {error.message}}
{title && }
{showSelect &&
Here are some suggestions to help you fix this issue:
Open your browser devtools Console tab to inspect the full error
If this seems like a GraphQL-related issue, you can inspect the GraphQL request in your browser devtools Network{' '}
tab to see what exactly is being sent to the server. You can then paste the query or mutation into{' '}
GraphiQL
{' '}
to debug it.
Note: these instructions will only appear during local development.
);
replaceComponent('ErrorCatcherContents', ErrorCatcherContents);
================================================
FILE: packages/vulcan-debug/lib/components/Groups.jsx
================================================
import React from 'react';
import { registerComponent } from 'meteor/vulcan:lib';
import Users from 'meteor/vulcan:users';
const Group = ({name, actions}) => {
return (
{name}
{actions.map((action, index) =>
{action}
)}
);
};
const Groups = props => {
return (
Groups
Name
Actions
{_.map(Users.groups, (group, key) => )}
);
};
registerComponent('Groups', Groups);
export default Groups;
================================================
FILE: packages/vulcan-debug/lib/components/I18n.jsx
================================================
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
* Note: for non-dynamically-loaded locales only
*
**/
function LocaleSwitcher(props, context) {
return (
);
registerComponent({
name: 'FormIntlItemLayout',
component: FormIntlItemLayout
});
class FormIntl extends PureComponent {
/*
Note: ideally we'd try to make sure to return the right path no matter
the order translations are stored in, but in practice we can't guarantee it
so we just use the order of the Locales array.
*/
getLocalePath = defaultIndex => {
return `${this.props.path}_intl.${defaultIndex}`;
};
render() {
const { name, formComponents } = this.props;
const FormComponents = mergeWithComponents(formComponents);
// do not pass FormIntl's own value, inputProperties, and intlInput props down
const properties = omit(
this.props,
'value',
'inputProperties',
'intlInput',
'nestedInput'
);
return (
{Locales.map((locale, i) => (
))}
);
}
}
FormIntl.propTypes = {
name: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
formComponents: PropTypes.object
};
registerComponent(
'FormIntl',
FormIntl,
getContext({
getLabel: PropTypes.func
})
);
================================================
FILE: packages/vulcan-forms/lib/components/FormLayout.jsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import { registerComponent } from 'meteor/vulcan:core';
const FormLayout = ({ FormComponents, commonProps, formProps, errorProps, repeatErrors, submitProps, children }) => (
{children}
{repeatErrors && }
);
export default FormLayout;
registerComponent('FormLayout', FormLayout);
================================================
FILE: packages/vulcan-forms/lib/components/FormNestedArray.jsx
================================================
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { registerComponent, instantiateComponent } from 'meteor/vulcan:core';
import _omit from 'lodash/omit';
import _get from 'lodash/get';
// Wraps the FormNestedItem, repeated for each object
// Allow for example to have a label per object
const FormNestedArrayInnerLayout = props => {
const { FormComponents, label, children, addItem, beforeComponent, afterComponent } = props;
return (
);
}
}
Tags.propTypes = {
name: PropTypes.string,
value: PropTypes.any,
label: PropTypes.string,
inputProperties: PropTypes.shape({
onChange: PropTypes.func,
}),
options: PropTypes.arrayOf(
PropTypes.shape({
value: PropTypes.any,
label: PropTypes.string,
})
),
};
export default Tags;
================================================
FILE: packages/vulcan-forms-tags/lib/export.js
================================================
import Tags from './components/Tags.jsx';
export default Tags;
================================================
FILE: packages/vulcan-forms-tags/package.js
================================================
Package.describe({
name: 'vulcan:forms-tags',
summary: 'Vulcan tag input package',
version: '1.16.9',
git: 'https://github.com/VulcanJS/Vulcan.git',
});
Package.onUse(function(api) {
api.use(['vulcan:core@=1.16.9', 'vulcan:forms@=1.16.9']);
api.mainModule('lib/export.js', ['client', 'server']);
});
================================================
FILE: packages/vulcan-forms-upload/README.md
================================================
# nova-upload
🏖🔭 Vulcan package extending `vulcan:forms` to upload images to Cloudinary from a drop zone.

Want to add this to your Vulcan instance? Read below:
# Installation
### 1. Meteor package
I would recommend that you clone this repo in your vulcan's `/packages` folder.
Then, open the `.meteor/packages` file and add at the end of the **Optional packages** section:
`xavcz:nova-forms-upload`
> **Note:** This is the version for Nova 1.0.0, running with GraphQL. *If you are looking for a version compatible with Nova "classic", you'll need to change the package's branch, like below. Then, refer to [the README for `nova-forms-upload` on Nova Classic](https://github.com/xavcz/nova-forms-upload/blob/nova-classic/README.md#installation)*
```bash
# only for Nova classic users (v0.27.5)
cd nova-forms-upload
git checkout nova-classic
```
### 2. NPM dependency
This package depends on the awesome `react-dropzone` ([repo](https://github.com/okonet/react-dropzone)), you need to install the dependency:
```
npm install react-dropzone cross-fetch
```
### 3. Cloudinary account
Create a [Cloudinary account](https://cloudinary.com) if you don't have one.
The upload to Cloudinary relies on **unsigned upload**:
> Unsigned upload is an option for performing upload directly from a browser or mobile application with no authentication signature, and without going through your servers at all. However, for security reasons, not all upload parameters can be specified directly when performing unsigned upload calls.
Unsigned upload options are controlled by [an upload preset](http://cloudinary.com/documentation/upload_images#upload_presets), so in order to use this feature you first need to enable unsigned uploading for your Cloudinary account from the [Upload Settings](https://cloudinary.com/console/settings/upload) page.
When creating your **preset**, you can define image transformations. I recommend to set something like 200px width & height, fill mode and auto quality. Once created, you will get a preset id.
It may look like this:

### 4. Nova Settings
Edit your `settings.json` and add inside the `public: { ... }` block the following entries with your own credentials:
```json
public: {
"cloudinaryCloudName": "YOUR_APP_NAME",
"cloudinaryPresets": {
"avatar": "YOUR_PRESET_ID",
"posts": "THE_SAME_OR_ANOTHER_PRESET_ID"
}
}
```
Picture upload in Nova is now enabled! Easy-peasy, right? 👯
### 5. Your custom package & custom fields
Make your custom package depends on this package: open `package.js` in your custom package and add `xavcz:nova-forms-upload` as a dependency, near by the other `nova:xxx` packages.
You can now use the `Upload` component as a classic form extension with [custom fields](https://www.youtube.com/watch?v=1yTT48xaSy8) like `nova:forms-tags` or `nova:embedly`.
**⚠️ Note:** Don't forget to update your query fragments wherever needed after defining your custom fields, else they will never be available!
## Image for posts
Let's say you want to enhance your posts with a custom image. In your custom package, your new custom field could look like this:
```js
// ... your imports
import { getComponent, getSetting } from 'meteor/nova:lib';
import Posts from 'meteor/nova:posts';
// extends Posts schema with a new field: 'image' 🏖
Posts.addField({
fieldName: 'image',
fieldSchema: {
type: String,
optional: true,
input: getComponent('Upload'),
canCreate: ['members'],
canUpdate: ['members'],
canRead: ['guests'],
form: {
options: {
preset: getSetting('cloudinaryPresets').posts // this setting refers to the transformation you want to apply to the image
},
}
}
});
```
## Avatar for users
Let's say you want to enable your users to upload their own avatar. In your custom package, your new custom field could look like this:
```js
// ... your imports
import { getComponent, getSetting } from 'meteor/nova:lib';
import Users from 'meteor/nova:users';
// extends Users schema with a new field: 'avatar' 👁
Users.addField({
fieldName: 'avatar',
fieldSchema: {
type: String,
optional: true,
input: getComponent('Upload'),
canCreate: ['members'],
canUpdate: ['members'],
canRead: ['guests'],
preload: true, // ⚠️ will preload the field for the current user!
form: {
options: {
preset: getSetting('cloudinaryPresets').avatar // this setting refers to the transformation you want to apply to the image
},
}
}
});
```
Adding the opportunity to upload an avatar comes with a trade-off: you also need to extend the behavior of the `Users.avatar` methods. You can do this by adding this snippet, in `custom_fields.js` for instance:
```js
const originalAvatarConstructor = Users.avatar;
// extends the Users.avatar function
Users.avatar = {
...originalAvatarConstructor,
getUrl(user) {
url = originalAvatarConstructor.getUrl(user);
return !!user && user.avatar ? user.avatar : url;
},
};
```
Now, you also need to update the query fragments related to `User` when you want the custom avatar to show up :)
## S3? Google Cloud?
Feel free to contribute to add new features and flexibility to this package :)
You are welcome to come chat about it [in the Slack chatroom](http://slack.vulcanjs.org)
## What about `nova:cloudinary` ?
This package and `nova:cloudinary` share a settings in common: `cloudinaryCloudName`. They are fully compatible.
Happy hacking! 🚀
================================================
FILE: packages/vulcan-forms-upload/lib/Upload.jsx
================================================
/*
This component supports uploading and storing an array of images.
Note also that an image can be stored as a simple string, or as an array of formats
(each format being itself an object).
### Deleting Images
When clearing an image, it is addeds to `deletedValues` and set to `null` in the array,
but the array item itself is not deleted. The entire array is then cleaned when submitting the form.
*/
import { Components, getSetting, registerSetting, registerComponent } from 'meteor/vulcan:lib';
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import Dropzone from 'react-dropzone';
import 'cross-fetch/polyfill'; // patch for browser which don't have fetch implemented
import set from 'lodash/set';
registerSetting('cloudinary.cloudName', null, 'Cloudinary cloud name (for image uploads)');
/*
Dropzone styles
*/
const baseStyle = {
borderWidth: 1,
borderStyle: 'dashed',
marginBottom: '10',
padding: '10',
};
const activeStyle = {
borderStyle: 'solid',
borderColor: '#6c6',
backgroundColor: '#eee',
};
const rejectStyle = {
borderStyle: 'solid',
borderColor: '#c66',
backgroundColor: '#eee',
};
/*
Get a URL from an image or an array of images
*/
const getImageUrl = imageOrImageArray => {
// if image is actually an array of formats, use first format
const image = Array.isArray(imageOrImageArray) ? imageOrImageArray[0] : imageOrImageArray;
// if image is an object, return secure_url; else return image itself
const imageUrl = typeof image === 'string' ? image : image.secure_url;
return imageUrl;
};
/*
Display a single image
*/
class Image extends PureComponent {
constructor() {
super();
this.clearImage = this.clearImage.bind(this);
}
clearImage(e) {
e.preventDefault();
this.props.clearImage(this.props.index);
}
render() {
return (
{this.props.loading && (
)}
Remove image
);
}
}
/*
Cloudinary Image Upload component
*/
class Upload extends PureComponent {
constructor(props, context) {
super(props);
const self = this;
// add callback to clean any preview or error values
// (when handling multiple images)
function uploadKeepRealImages(data) {
if (Array.isArray(self.props.value)) {
// keep only "real" images
const images = self.getImages({
includePreviews: false,
includeDeleted: false,
});
// replace images in `data` object with real images
set(data, self.props.path, images);
}
return data;
}
context.addToSubmitForm(uploadKeepRealImages);
}
state = { uploading: false };
/*
Find out field type
*/
getFieldType = () => {
return this.props.datatype && this.props.datatype[0].type;
};
/*
Check the field's type to decide if the component should handle
multiple image uploads or not. Default to yes.
*/
enableMultiple = () => {
return this.getFieldType() !== String || this.props.maxCount !== 1;
};
/*
Whether to disable the dropzone.
*/
isDisabled = () => {
return this.state.uploading || this.props.maxCount <= this.getImages({ includeDeleted: false }).length;
};
/*
When an image is uploaded
*/
onDrop = files => {
const promises = [];
const imagesCount = this.getImages().length;
this.props.clearFieldErrors(this.props.path);
// set the component in upload mode
this.setState({
uploading: true,
});
// request url to cloudinary
const cloudinaryUrl = `https://api.cloudinary.com/v1_1/${getSetting('cloudinary.cloudName')}/upload`;
// trigger a request for each file
files.forEach((file, index) => {
// figure out update path for current image
const updateIndex = imagesCount + index;
const updatePath = this.getFieldType() === String ? this.props.path : `${this.props.path}.${updateIndex}`;
// build preview object
const previewObject = {
secure_url: file.preview,
loading: true,
preview: true,
};
// update current values using preview object
this.props.updateCurrentValues({ [updatePath]: previewObject });
// request body
const body = new FormData();
body.append('file', file);
body.append('upload_preset', this.props.options.preset);
// post request to cloudinary
promises.push(
fetch(cloudinaryUrl, {
method: 'POST',
body,
})
.then(res => res.json()) // json-ify the readable strem
.then(body => {
if (body.error) {
// eslint-disable-next-line no-console
console.log(body.error);
this.props.throwError({
id: 'upload.error',
path: this.props.path,
message: body.error.message,
});
const errorObject = {
...previewObject,
loading: false,
error: true,
};
this.props.updateCurrentValues({ [updatePath]: errorObject });
return null;
} else {
// use the https:// url given by cloudinary; or eager property if using transformations
const imageObject = body.eager ? body.eager : body.secure_url;
this.props.updateCurrentValues({ [updatePath]: imageObject });
return imageObject;
}
})
.catch(error => {
// eslint-disable-next-line no-console
console.log(error);
this.props.throwError({
id: 'upload.error',
path: this.props.path,
message: error.message,
});
})
);
});
Promise.all(promises).then(values => {
// console.log(values);
// set the uploading status to false
this.setState({
uploading: false,
});
});
};
isDeleted = index => {
return this.props.deletedValues.includes(`${this.props.path}.${index}`);
};
/*
Remove the image at `index`
*/
clearImage = index => {
this.props.updateCurrentValues({ [`${this.props.path}.${index}`]: null });
};
/*
Get images, with or without previews/deleted images
*/
getImages = (args = {}) => {
const { includePreviews = true, includeDeleted = false } = args;
let images = this.props.value;
// if images is an empty string, null, etc. just return an empty array
if (!images) {
return [];
}
// if images is not array, make it one (for backwards compatibility)
if (!Array.isArray(images)) {
images = [images];
}
// remove previews if needed
images = includePreviews ? images : images.filter(image => !image.preview);
// remove deleted images
images = includeDeleted ? images : images.filter((image, index) => !this.isDeleted(index));
return images;
};
render() {
const { uploading } = this.state;
const images = this.getImages({ includeDeleted: true });
return (
);
}
}
Upload.propTypes = {
name: PropTypes.string,
value: PropTypes.any,
label: PropTypes.string,
};
Upload.contextTypes = {
addToSubmitForm: PropTypes.func,
};
registerComponent('Upload', Upload);
export default Upload;
================================================
FILE: packages/vulcan-forms-upload/lib/Upload.scss
================================================
.upload-field {
display: flex;
align-items: center;
justify-content: flex-start;
}
.dropzone-base {
border: 4px dashed #ccc;
padding: 30px;
transition: "all 0.5s";
width: 250px;
cursor: pointer;
color: #ccc;
margin-right: 10px;
position: relative;
}
.upload-uploading{
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: rgba(255,255,255,0.8);
display: flex;
justify-content: center;
align-items: center;
span{
display: block;
font-size: 1.5rem;
}
}
.dropzone-active {
border: #4FC47F 4px solid;
}
.dropzone-reject {
border: #DD3A0A 4px solid;
}
.upload-images{
display: flex;
flex-direction: row;
flex-wrap: wrap;
// justify-content: space-between;
// align-items: center;
}
.upload-image{
margin-right: 10px;
a, img{
display: block;
text-align: center;
}
a{
font-size: 0.8rem;
}
}
.upload-image-contents{
position: relative;
}
.upload-loading{
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: rgba(255,255,255,0.8);
display: flex;
justify-content: center;
align-items: center;
}
.upload-disabled{
.dropzone-base{
background-image: url("data:image/svg+xml,%3Csvg width='40' height='40' viewBox='0 0 40 40' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23cccccc' fill-opacity='0.4' fill-rule='evenodd'%3E%3Cpath d='M0 40L40 0H20L0 20M40 40V20L20 40'/%3E%3C/g%3E%3C/svg%3E");
}
}
.upload-image-error{
.upload-image-contents{
position: relative;
&:after{
content: " ";
display: block;
position: absolute;
height: 100%;
width: 100%;
top: 0;
right: 0;
left: 0;
right: 0;
background-color: rgba(255, 255, 255, 0.6);
background-image: url("data:image/svg+xml,%3Csvg width='40' height='40' viewBox='0 0 40 40' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23ff0000' fill-opacity='0.4' fill-rule='evenodd'%3E%3Cpath d='M0 40L40 0H20L0 20M40 40V20L20 40'/%3E%3C/g%3E%3C/svg%3E");
}
}
}
================================================
FILE: packages/vulcan-forms-upload/lib/i18n.js
================================================
import { addStrings } from 'meteor/vulcan:core';
addStrings('en', {
'upload.prompt': 'Drop an image here, or click to select an image to upload.',
'upload.uploading': 'Uploading…'
});
================================================
FILE: packages/vulcan-forms-upload/lib/modules.js
================================================
import Upload from './Upload.jsx';
import './i18n.js';
export default Upload;
================================================
FILE: packages/vulcan-forms-upload/package.js
================================================
Package.describe({
name: 'vulcan:forms-upload',
summary: 'Vulcan package extending vulcan:forms to upload images to Cloudinary from a drop zone.',
version: '1.16.9',
git: 'https://github.com/xavcz/nova-forms-upload.git',
});
Package.onUse(function(api) {
api.use(['vulcan:core@=1.16.9', 'vulcan:forms@=1.16.9', 'vulcan:scss@4.12.0']);
api.addFiles(['lib/Upload.scss'], 'client');
api.mainModule('lib/modules.js', ['client', 'server']);
});
================================================
FILE: packages/vulcan-i18n/README.md
================================================
Vulcan i18n package.
================================================
FILE: packages/vulcan-i18n/lib/client/main.js
================================================
export * from '../modules/index.js';
================================================
FILE: packages/vulcan-i18n/lib/modules/context.js
================================================
import React from 'react';
const IntlContext = React.createContext({
locale: '',
key: '',
messages: [],
});
export default IntlContext;
================================================
FILE: packages/vulcan-i18n/lib/modules/index.js
================================================
import { registerSetting } from 'meteor/vulcan:lib';
registerSetting('locale', 'en-US', 'Your app\'s locale (“en”, “fr”, etc.)');
export { default as FormattedMessage } from './message.js';
export { intlShape } from './shape.js';
export { default as IntlProvider } from './provider.js';
export { default as IntlContext } from './context.js';
export { default as useIntl } from './useIntl.js';
================================================
FILE: packages/vulcan-i18n/lib/modules/message.js
================================================
import React, { Component } from 'react';
import { intlShape } from './shape';
import { registerComponent } from 'meteor/vulcan:lib';
const FormattedMessage = ({ id, values, defaultMessage = '', html = false, className = '' }, { intl }) => {
let message = intl.formatMessage({ id, defaultMessage }, values);
const cssClass = `i18n-message ${className}`;
// if message is empty, use [id]
if (message === '') {
message = `[${id}]`;
}
return html ?
:
{message};
};
FormattedMessage.contextTypes = {
intl: intlShape
};
registerComponent('FormattedMessage', FormattedMessage);
export default FormattedMessage;
================================================
FILE: packages/vulcan-i18n/lib/modules/provider.js
================================================
import React, { Component } from 'react';
import { getString } from 'meteor/vulcan:lib';
import { intlShape } from './shape.js';
export default class IntlProvider extends Component {
formatMessage = ({ id, defaultMessage }, values = null) => {
const { messages, locale } = this.props;
return getString({ id, defaultMessage, values, messages, locale });
};
formatStuff = something => {
return something;
};
getChildContext() {
return {
intl: {
formatDate: this.formatStuff,
formatTime: this.formatStuff,
formatRelative: this.formatStuff,
formatNumber: this.formatStuff,
formatPlural: this.formatStuff,
formatMessage: this.formatMessage,
formatHTMLMessage: this.formatStuff,
now: this.formatStuff,
locale: this.props.locale,
},
};
}
render() {
return this.props.children;
}
}
IntlProvider.childContextTypes = {
intl: intlShape,
};
================================================
FILE: packages/vulcan-i18n/lib/modules/shape.js
================================================
/*
* Copyright 2015, Yahoo Inc.
* Copyrights licensed under the New BSD License.
* See the accompanying LICENSE file for terms.
*/
import PropTypes from 'prop-types';
const { bool, number, string, func, object, oneOf, shape, any } = PropTypes;
const localeMatcher = oneOf(['best fit', 'lookup']);
const narrowShortLong = oneOf(['narrow', 'short', 'long']);
const numeric2digit = oneOf(['numeric', '2-digit']);
const funcReq = func.isRequired;
export const intlConfigPropTypes = {
locale: string,
formats: object,
messages: object,
textComponent: any,
defaultLocale: string,
defaultFormats: object,
};
export const intlFormatPropTypes = {
formatDate: funcReq,
formatTime: funcReq,
formatRelative: funcReq,
formatNumber: funcReq,
formatPlural: funcReq,
formatMessage: funcReq,
formatHTMLMessage: funcReq,
};
export const intlShape = shape({
...intlConfigPropTypes,
...intlFormatPropTypes,
formatters: object,
now: funcReq,
});
export const messageDescriptorPropTypes = {
id: string.isRequired,
description: string,
defaultMessage: string,
};
export const dateTimeFormatPropTypes = {
localeMatcher,
formatMatcher: oneOf(['basic', 'best fit']),
timeZone: string,
hour12: bool,
weekday: narrowShortLong,
era: narrowShortLong,
year: numeric2digit,
month: oneOf(['numeric', '2-digit', 'narrow', 'short', 'long']),
day: numeric2digit,
hour: numeric2digit,
minute: numeric2digit,
second: numeric2digit,
timeZoneName: oneOf(['short', 'long']),
};
export const numberFormatPropTypes = {
localeMatcher,
style: oneOf(['decimal', 'currency', 'percent']),
currency: string,
currencyDisplay: oneOf(['symbol', 'code', 'name']),
useGrouping: bool,
minimumIntegerDigits: number,
minimumFractionDigits: number,
maximumFractionDigits: number,
minimumSignificantDigits: number,
maximumSignificantDigits: number,
};
export const relativeFormatPropTypes = {
style: oneOf(['best fit', 'numeric']),
units: oneOf(['second', 'minute', 'hour', 'day', 'month', 'year']),
};
export const pluralFormatPropTypes = {
style: oneOf(['cardinal', 'ordinal']),
};
================================================
FILE: packages/vulcan-i18n/lib/modules/useIntl.js
================================================
import React, { useContext } from 'react';
import IntlContext from './context';
export default function useIntl() {
const intl = useContext(IntlContext);
return intl;
}
================================================
FILE: packages/vulcan-i18n/lib/server/graphql.js
================================================
import { addGraphQLQuery, addGraphQLResolvers, addGraphQLSchema, Locales, getLocale, getStrings } from 'meteor/vulcan:lib';
// const localEnum = `enum LocaleID {
// ${Locales.map(locale => locale.id).join('/n')}
// }`;
// console.log(Locales)
// console.log(localEnum)
// addGraphQLSchema(localEnum);
const localeType = `type Locale {
id: String,
label: String
dynamic: Boolean
strings: JSON
}`;
addGraphQLSchema(localeType);
const locale = async (root, { localeId }, context) => {
const locale = getLocale(localeId);
const strings = getStrings(localeId);
const localeObject = { ...locale, strings };
return localeObject;
};
addGraphQLQuery('locale(localeId: String): Locale');
addGraphQLResolvers({ Query: { locale } });
================================================
FILE: packages/vulcan-i18n/lib/server/main.js
================================================
export * from '../modules/index.js';
import './graphql.js';
================================================
FILE: packages/vulcan-i18n/package.js
================================================
Package.describe({
name: 'vulcan:i18n',
summary: 'i18n client polyfill',
version: '1.16.9',
git: 'https://github.com/VulcanJS/Vulcan',
});
Package.onUse(function(api) {
api.use(['vulcan:lib@=1.16.9']);
api.mainModule('lib/server/main.js', 'server');
api.mainModule('lib/client/main.js', 'client');
});
Package.onTest(function(api) {
api.use(['ecmascript', 'meteortesting:mocha', 'vulcan:test', 'vulcan:i18n']);
api.mainModule('./test/index.js');
});
================================================
FILE: packages/vulcan-i18n/test/index.js
================================================
import './provider.test.js';
================================================
FILE: packages/vulcan-i18n/test/provider.test.js
================================================
import IntlProvider from '../lib/modules/provider';
import React from 'react';
import expect from 'expect';
import { shallow } from 'enzyme';
import { addStrings } from 'meteor/vulcan:core';
import { initComponentTest } from 'meteor/vulcan:test';
initComponentTest();
// constants for formatMessage
const defaultMessage = 'default';
const stringId = 'test_string';
const ENTestString = 'English test string';
const FRTestString = 'Phrase test en Français';
const valueStringId = 'valueStringId';
const valueStringValue = 'Vulcan';
const valueTestStringStatic = 'the value is ';
const valueTestStringDynamic = 'testValue';
const valueTestString = `${valueTestStringStatic}{${valueTestStringDynamic}}`;
// add the strings for formatMessage
addStrings('en', {
[stringId]: ENTestString,
[valueStringId]: valueTestString,
});
addStrings('fr', {
[stringId]: FRTestString,
});
describe('vulcan:i18n/IntlProvider', function() {
it('shallow render', function() {
const wrapper = shallow();
expect(wrapper).toBeDefined();
});
describe('formatMessage', function() {
it('format a message according to locale', function() {
const wrapper = shallow();
const ENString = wrapper.instance().formatMessage({ id: stringId });
expect(ENString).toEqual(ENTestString);
wrapper.setProps({ locale: 'fr' });
const FRString = wrapper.instance().formatMessage({ id: stringId });
expect(FRString).toEqual(FRTestString);
});
it('format a message according to a value', function() {
const wrapper = shallow();
const dynamicString = wrapper
.instance()
.formatMessage({ id: valueStringId }, { [valueTestStringDynamic]: valueStringValue });
expect(dynamicString).toEqual(valueTestStringStatic + valueStringValue);
});
it('return a default message when no string is found', function() {
const wrapper = shallow();
const ENString = wrapper.instance().formatMessage({
id: 'unknownStringId',
defaultMessage: defaultMessage,
});
expect(ENString).toEqual(defaultMessage);
});
});
});
================================================
FILE: packages/vulcan-i18n-en-us/README.md
================================================
Vulcan i18n en_US package.
================================================
FILE: packages/vulcan-i18n-en-us/lib/en_US.js
================================================
import { addStrings } from 'meteor/vulcan:core';
addStrings('en', {
'accounts.error_incorrect_password': 'Incorrect password',
'accounts.error_email_required': 'Email required',
'accounts.error_email_already_exists': 'Email already exists',
'accounts.error_invalid_email': 'Invalid email',
'accounts.error_minchar': 'Your password is too short',
'accounts.error_username_required': 'Username required',
'accounts.error_accounts_': '',
'accounts.error_unknown': 'Unknown error',
'accounts.error_user_not_found': 'User not found',
'accounts.error_username_already_exists': 'Username already exists',
'accounts.enter_username_or_email': 'Enter username or email',
'accounts.error_internal_server_error': 'Internal server error',
'accounts.error_token_expired': 'Invalid password reset token',
'accounts.username_or_email': 'Username or email',
'accounts.enter_username': 'Enter username',
'accounts.username': 'Username',
'accounts.enter_email': 'Enter email',
'accounts.email': 'Email',
'accounts.enter_password': 'Enter password',
'accounts.password': 'Password',
'accounts.choose_password': 'Choose password',
'accounts.change_password': 'Change password',
'accounts.reset_your_password': 'Reset your password',
'accounts.set_password': 'Set password',
'accounts.enter_new_password': 'Enter new password',
'accounts.new_password': 'New password',
'accounts.forgot_password': 'Forgot password',
'accounts.sign_up': 'Sign up',
'accounts.sign_in': 'Sign in',
'accounts.sign_out': 'Sign out',
'accounts.cancel': 'Cancel',
'accounts.or_use': 'or use',
'accounts.info_email_sent': 'Email sent.',
'accounts.info_password_changed': 'Password changed.',
'accounts.logging_in': 'Logging in…',
'accounts.email_verified': 'Your email address has been verified.',
'forms.submit': 'Submit',
'forms.cancel': 'Cancel',
'forms.select_option': '-- select option --',
'forms.add_nested_field': 'Add a {label}',
'forms.delete_nested_field': 'Delete this {label}?',
'forms.delete': 'Delete',
'forms.delete_field': 'Delete the field?',
'forms.delete_confirm': 'Delete document?',
'forms.revert': 'Revert',
'forms.confirm_discard': 'Discard changes?',
'forms.day': 'Day',
'forms.month': 'Month',
'forms.year': 'Year',
'forms.start_adornment_url_icon': 'Web icon',
'forms.start_adornment_email_icon': 'Email icon',
'forms.start_adornment_social_icon': 'Social icon',
'forms.clear_field': 'Clear field value',
'users.profile': 'Profile',
'users.complete_profile': 'Complete your Profile',
'users.profile_completed': 'Profile completed.',
'users.edit_account': 'Edit Account',
'users.edit_success': 'User “{name}” edited',
'users.log_in': 'Log In',
'users.sign_up': 'Sign Up',
'users.sign_up_log_in': 'Sign Up/Log In',
'users.log_out': 'Log Out',
'users.bio': 'Bio',
'users.displayName': 'Display Name',
'users.email': 'Email',
'users.twitterUsername': 'Twitter Username',
'users.website': 'Website',
'users.groups': 'Groups',
'users.avatar': 'Avatar',
'users.notifications': 'Notifications',
'users.notifications_users': 'New Users Notifications',
'users.notifications_posts': 'New Posts Notifications',
'users.newsletter_subscribeToNewsletter': 'Subscribe to newsletter',
'users.users_admin': 'Admin',
'users.admin': 'Admin',
'users.host': 'Team member',
'users.isAdmin': 'Admin',
'users.posts': 'Posts',
'users.upvoted_posts': 'Upvoted Posts',
'users.please_log_in': 'Please log in',
'users.please_sign_up_log_in': 'Please sign up or log in',
'users.cannot_post': 'Sorry, you do not have permission to post at this time',
'users.cannot_comment': 'Sorry, you do not have permission to comment at this time',
'users.subscribe': "Subscribe to this user's posts",
'users.unsubscribe': "Unsubscribe to this user's posts",
'users.subscribed': 'You have subscribed to “{name}” posts.',
'users.unsubscribed': 'You have unsubscribed from “{name}” posts.',
'users.subscribers': 'Subscribers',
'users.delete': 'Delete user',
'users.delete_confirm': 'Delete this user?',
'users.email_already_taken': 'This email is already taken: {value}',
settings: 'Settings',
'settings.json_message': 'Note: settings already provided in your settings.json file will be disabled.',
'settings.edit': 'Edit Settings',
'settings.edited': 'Settings edited (please reload).',
'settings.title': 'Title',
'settings.siteUrl': 'Site URL',
'settings.tagline': 'Tagline',
'settings.description': 'Description',
'settings.siteImage': 'Site Image',
'settings.defaultEmail': 'Default Email',
'settings.mailUrl': 'Mail URL',
'settings.scoreUpdate': 'Score Update',
'settings.postInterval': 'Post Interval',
'settings.RSSLinksPointTo': 'RSS Links Point To',
'settings.commentInterval': 'Comment Interval',
'settings.maxPostsPerDay': 'Max Posts Per Day',
'settings.startInvitesCount': 'Start Invites Count',
'settings.postsPerPage': 'Posts Per Page',
'settings.logoUrl': 'Logo URL',
'settings.logoHeight': 'Logo Height',
'settings.logoWidth': 'Logo Width',
'settings.faviconUrl': 'Favicon URL',
'settings.twitterAccount': 'Twitter Account',
'settings.facebookPage': 'Facebook Page',
'settings.googleAnalyticsId': 'Google Analytics ID',
'settings.locale': 'Locale',
'settings.requireViewInvite': 'Require View Invite',
'settings.requirePostInvite': 'Require Post Invite',
'settings.requirePostsApproval': 'Require Posts Approval',
'settings.scoreUpdateInterval': 'Score Update Interval',
'app.loading': 'Loading…',
'app.404': "Sorry, we couldn't find what you were looking for.",
'app.empty_input': 'Single resolver cannot receive an empty input object.',
'app.missing_document': "Sorry, we couldn't find the document you were looking for.",
'app.powered_by': 'Built with Vulcan.js',
'app.or': 'Or',
'app.noPermission': 'Sorry, you do not have the permission to do this at this time.',
'app.operation_not_allowed': 'Sorry, you don\'t have the rights to perform the operation "{operationName}"',
'app.document_not_found': 'Document not found (id: {value})',
'app.no_permissions_defined': 'No permissions defined for operation [{operationName}]',
'app.disallowed_property_detected': 'Disallowed property detected: {value}',
'app.something_bad_happened': 'Something bad happened...',
'app.embedly_not_authorized':
'Invalid Embedly API key provided in the settings file. To find your key, log into https://app.embed.ly -> API',
'app.defaultError': '{defaultMessage}',
'app.please_sign_up_log_in': 'Please sign up or log in',
'app.no_access_permissions': 'Sorry, you are not allowed to access this page.',
'cards.edit': 'Edit',
'datatable.new': 'New',
'datatable.edit': 'Edit',
'datatable.search': 'Search',
'datatable.submit': 'Submit',
admin: 'Admin',
notifications: 'Notifications',
'errors.expectedType': 'Expected type {dataType} for field “{label}”, received “{value}” instead.',
'errors.required': 'Field “{label}” is required.',
'errors.minString': 'Field “{label}” needs to have at least {min} characters',
'errors.maxString': 'Field “{label}” is limited to {max} characters.',
'errors.generic': 'Sorry, something went wrong: {errorMessage}.',
'errors.generic_report': 'Sorry, something went wrong: {errorMessage}. An error report has been generated.',
'errors.minNumber': 'Field “{label}” must be higher than {min}. ',
'errors.maxNumber': 'Field “{label}” must be lower than {max}. ',
'errors.minCount': 'There needs to be at least {count} in field “{label}”.',
'errors.maxCount': 'Field “{label}” is only allowed {count}.',
'errors.regEx': 'Field “{label}”: wrong formatting',
'errors.badDate': 'Field “{label}” is not a date.',
'errors.notAllowed': 'The value for field “{label}” is not allowed.',
'errors.noDecimal': 'The value for field “{label}” must not be a decimal number.',
//TODO other simple schema errors
'errors.minNumberExclusive': '',
'errors.maxNumberExclusive': '',
'errors.keyNotInSchema': '',
});
================================================
FILE: packages/vulcan-i18n-en-us/package.js
================================================
Package.describe({
name: 'vulcan:i18n-en-us',
summary: 'Vulcan i18n package (en_US)',
version: '1.16.9',
git: 'https://github.com/VulcanJS/Vulcan.git',
});
Package.onUse(function(api) {
api.use(['vulcan:core@=1.16.9']);
api.addFiles(['lib/en_US.js'], ['client', 'server']);
});
================================================
FILE: packages/vulcan-i18n-es-es/README.md
================================================
Vulcan i18n es_ES package.
================================================
FILE: packages/vulcan-i18n-es-es/lib/es_ES.js
================================================
import { addStrings } from 'meteor/vulcan:core';
addStrings('es', {
'accounts.error_incorrect_password': 'Contraseña Incorrecta',
'accounts.error_email_required': 'Se requiere correo electrónico',
'accounts.error_email_already_exists': 'El correo electrónico ya existe',
'accounts.error_invalid_email': 'Correo electrónico no válido',
'accounts.error_minchar': 'Su contraseña es muy corta',
'accounts.error_username_required': 'Nombre de usuario requerido',
'accounts.error_accounts_': '',
'accounts.error_unknown': 'Error desconocido',
'accounts.error_user_not_found': 'Usuario no encontrado',
'accounts.error_username_already_exists': 'El nombre de usuario ya existe',
'accounts.enter_username_or_email': 'Ingresar nombre de usuario o correo electrónico',
'accounts.error_internal_server_error': 'Error interno del servidor',
'accounts.error_token_expired': 'Token de restablecimiento de contraseña inválido',
'accounts.username_or_email': 'Nombre de usuario o correo electrónico',
'accounts.enter_username': 'Ingresar nombre de usuario',
'accounts.username': 'Nombre de usuario',
'accounts.enter_email': 'Ingresar correo electrónico',
'accounts.email': 'Correo electrónico',
'accounts.enter_password': 'Ingresar contraseña',
'accounts.password': 'Contraseña',
'accounts.choose_password': 'Elegir contraseña',
'accounts.change_password': 'Cambiar contraseña',
'accounts.reset_your_password': 'Restablecer su contraseña',
'accounts.set_password': 'Establecer contraseña',
'accounts.enter_new_password': 'Introduzca una nueva contraseña',
'accounts.new_password': 'Nueva contraseña',
'accounts.forgot_password': 'Olvidé mi contraseña',
'accounts.sign_up': 'Registrarse',
'accounts.sign_in': 'Iniciar sesión',
'accounts.sign_out': 'Cerrar sesión',
'accounts.cancel': 'Cancelar',
'accounts.or_use': 'o usar',
'accounts.info_email_sent': 'Correo electrónico enviado.',
'accounts.info_password_changed': 'Contraseña cambiada.',
'accounts.logging_in': 'Iniciando sesión…',
'accounts.email_verified': 'Tu dirección de email ha sido verificada.',
'forms.submit': 'Enviar',
'forms.cancel': 'Cancelar',
'forms.select_option': '-- seleccionar opción --',
'forms.add_nested_field': 'Agregar un {label}',
'forms.delete_nested_field': '¿Eliminar este {label}?',
'forms.delete': 'Eliminar',
'forms.delete_field': '¿Eliminar campo?',
'forms.delete_confirm': '¿Eliminar documento?',
'forms.revert': 'Revertir',
'forms.confirm_discard': '¿Descartar los cambios?',
'forms.start_adornment_url_icon': 'Icono de internet',
'forms.start_adornment_email_icon': 'Icono de correo electrónico',
'forms.start_adornment_social_icon': '',
'users.profile': 'Perfil',
'users.complete_profile': 'Complete su perfil',
'users.profile_completed': 'Perfil completado.',
'users.edit_account': 'Editar cuenta',
'users.edit_success': 'Usuario “{name}” editado',
'users.log_in': 'Iniciar sesión',
'users.sign_up': 'Registrarse',
'users.sign_up_log_in': 'Registrarse/ Iniciar sesión',
'users.log_out': 'Cerrar sesión',
'users.bio': 'Bio',
'users.displayName': 'Nombre a Mostrar',
'users.email': 'Correo electrónico',
'users.twitterUsername': 'Nombre de usuario de Twitter',
'users.website': 'Sitio web',
'users.groups': 'Grupos',
'users.avatar': 'Avatar',
'users.notifications': 'Notificaciones',
'users.notifications_users': 'Notificaciones de nuevos usuarios',
'users.notifications_posts': 'Notificaciones de publicaciones nuevas',
'users.newsletter_subscribeToNewsletter': 'Suscribirse al boletín informativo',
'users.users_admin': 'Admin',
'users.admin': 'Admin',
'users.host': 'Miembro del equipo',
'users.isAdmin': 'Administrador',
'users.posts': 'Publicaciones',
'users.upvoted_posts': 'Publicaciones modificadas',
'users.please_log_in': 'Inicia sesión',
'users.please_sign_up_log_in': 'Regístrese o inicie sesión',
'users.cannot_post': 'Lo siento, no tienes permiso para publicar en este momento',
'users.cannot_comment': 'Lo siento, no tienes permiso para comentar en este momento',
'users.subscribe': 'Suscribirse a las publicaciones de este usuario',
'users.unsubscribe': 'Anular la suscripción a las publicaciones de este usuario',
'users.subscribed': 'Te has suscrito a “{name}” publicaciones.',
'users.unsubscribed': 'Ha cancelado la suscripción a publicaciones de “{name}”.',
'users.subscribers': 'Suscriptores',
'users.delete': 'Eliminar usuario',
'users.delete_confirm': '¿Eliminar este usuario?',
'users.email_already_taken': 'Este correo electrónico ya está tomado: {value}',
'settings': 'Configuración',
'settings.json_message': 'Nota: la configuración ya provista en su archivo settings.json code> estará deshabilitada.',
'settings.edit': 'Editar configuración',
'settings.edited': 'Configuración editada (recargue).',
'settings.title': 'Título',
'settings.siteUrl': 'URL del sitio',
'settings.tagline': 'Tagline',
'settings.description': 'Descripción',
'settings.siteImage': 'Imagen del sitio',
'settings.defaultEmail': 'Correo electrónico predeterminado',
'settings.mailUrl': 'Mail URL',
'settings.scoreUpdate': 'Actualización de puntaje',
'settings.postInterval': 'Intervalo de publicación',
'settings.RSSLinksPointTo': 'RSS Links Point To',
'settings.commentInterval': 'Intervalo de comentarios',
'settings.maxPostsPerDay': 'Publicaciones máximas por día',
'settings.startInvitesCount': 'Iniciar recuentos de invitaciones',
'settings.postsPerPage': 'Publicaciones por página',
'settings.logoUrl': 'URL del Logotipo',
'settings.logoHeight': 'Alto del Logotipo',
'settings.logoWidth': 'Ancho del Logotipo',
'settings.faviconUrl': 'Favicon URL',
'settings.twitterAccount': 'Cuenta de Twitter',
'settings.facebookPage': 'Página de Facebook',
'settings.googleAnalyticsId': 'ID de Google Analytics',
'settings.locale': 'Locale',
'settings.requireViewInvite': 'Requiere Invitación para ver',
'settings.requirePostInvite': 'Requiere Invitación para publicar',
'settings.requirePostsApproval': 'Requiere aprobación de publicaciones',
'settings.scoreUpdateInterval': 'Intervalo de actualización de puntuación',
'app.loading': 'Cargando…',
'app.404': 'Disculpa, no pudimos encontrar lo que estabas buscando.',
'app.missing_document': 'Lo sentimos, no pudimos encontrar el documento que estaba buscando.',
'app.powered_by': 'Construido con VulcanJS',
'app.or': 'O',
'app.noPermission': 'Lo siento, no tiene permiso para hacer esto en este momento.',
'app.operation_not_allowed': 'Lo sentimos, no tiene los derechos para realizar la operación “{operationName}”',
'app.document_not_found': 'Documento no encontrado (id: {value})',
'app.disallowed_property_detected': 'Propiedad no permitida detectada: {value}',
'app.something_bad_happened': 'Algo malo pasó...',
'app.embedly_not_authorized': 'Clave API incrustada no válida incluida en el archivo de configuración. Para encontrar su clave, inicie sesión en https://app.embed.ly -> API',
'app.defaultError': '{defaultMessage}',
'app.please_sign_up_log_in': 'Please sign up or log in',
'app.no_access_permissions': 'Sorry, you are not allowed to access this page.',
'cards.edit': 'Editar',
'datatable.new': 'Nuevo',
'datatable.edit': 'Editar',
'admin': 'Administrador',
'notifications': 'Notificaciones',
'errors.expectedType': 'Se esperaba un campo “{label}” de tipo {dataType}, se ha recibido “{value}” en su lugar.',
'errors.required': 'El campo “{label}” es obligatorio.',
'errors.minString': 'El campo “{label}” debe tener al menos {max} caracteres.',
'errors.maxString': 'El campo “{label}” está limitado a {max} caracteres.',
'errors.generic':'Ha ocurrido un error: {errorMessage}',
'errors.generic_report':'Algo ha ido mal: {errorMessage}. Se ha reportado el error.',
'errors.minNumber':'El campo “{label}” debe ser superior a {min}.',
'errors.maxNumber':'El campo “{label}” debe ser inferior a {max}.',
'errors.minCount':'El campo “{label}” debe tener al menos {count}.',
'errors.maxCount':'El campo “{label}” está limitado a {count}.',
'errors.regEx':'El campo “{label}” está mal formateado.',
'errors.badDate':'El campo “{label}” debe ser una fecha.',
'errors.notAllowed':'El valor del campo “{label}” no està permitido.',
'errors.noDecimal':'El campo “{label}” no puede ser un decimal.',
'errors.minNumberExclusive':'',
'errors.maxNumberExclusive':'',
'errors.keyNotInSchema':'',
});
================================================
FILE: packages/vulcan-i18n-es-es/package.js
================================================
Package.describe({
name: 'vulcan:i18n-es-es',
summary: 'Vulcan i18n package (es_ES)',
version: '1.16.9',
git: 'https://github.com/VulcanJS/Vulcan.git',
});
Package.onUse(function(api) {
api.use(['vulcan:core@=1.16.9']);
api.addFiles(['lib/es_ES.js'], ['client', 'server']);
});
================================================
FILE: packages/vulcan-i18n-fa-ir/README.md
================================================
Vulcan i18n fa_IR package.
================================================
FILE: packages/vulcan-i18n-fa-ir/lib/fa_IR.js
================================================
import { addStrings } from 'meteor/vulcan:core';
addStrings('fa-IR', {
'accounts.error_incorrect_password': 'رمزعبور نادرست است',
'accounts.error_email_required': 'ایمیل الزامی است',
'accounts.error_email_already_exists': 'ایمیل از قبل وجود دارد',
'accounts.error_invalid_email': 'ایمیل نامعتبر است',
'accounts.error_minchar': 'رمزعبور شما بیش از حد کپتاه است',
'accounts.error_username_required': 'نام کاربری الزامی است',
'accounts.error_accounts_': '',
'accounts.error_unknown': 'خظای ناشناخته',
'accounts.error_user_not_found': 'کاربر یافت نشد',
'accounts.error_username_already_exists': 'نام کاربری از قبل وجود دارد',
'accounts.enter_username_or_email': 'نام کاربری یا ایمیل را وارد نمایید',
'accounts.error_internal_server_error': 'خطای سرور',
'accounts.error_token_expired': 'توکن بازیابی رمزعبور اشتباه است',
'accounts.username_or_email': 'نام کاربری یا ایمیل',
'accounts.enter_username': 'نام کاربری را وارد نمایید',
'accounts.username': 'نام کاربری',
'accounts.enter_email': 'ایمیل را وارد نمایید',
'accounts.email': 'ایمیل',
'accounts.enter_password': 'رمزعبور را وارد نمایید',
'accounts.password': 'رمزعبور',
'accounts.choose_password': 'انتخاب رمزعبور',
'accounts.change_password': 'تغییر رمزعبور',
'accounts.reset_your_password': 'بازیابی رمزعبور',
'accounts.set_password': 'تنظیم رمزعبور',
'accounts.enter_new_password': 'رمزعبور جدید را وارد نمایید',
'accounts.new_password': 'رمزعبور جدید',
'accounts.forgot_password': 'فراموشی رمزعبور',
'accounts.sign_up': 'ثبت نام',
'accounts.sign_in': 'ورود',
'accounts.sign_out': 'خروج',
'accounts.cancel': 'انصراف',
'accounts.or_use': 'یا استفاده از',
'accounts.info_email_sent': 'ایمیل ارسال شد.',
'accounts.info_password_changed': 'رمزعبور تغییر یافت.',
'accounts.logging_in': 'درحال ورود...',
'accounts.email_verified': 'آدرس ایمیل شما تایید شد.',
'forms.submit': 'تایید',
'forms.cancel': 'انصراف',
'forms.select_option': '-- انتخاب گزینه --',
'forms.add_nested_field': '{label}أضف',
'forms.delete_nested_field': '{label}اضافه کردن',
'forms.delete': 'حذف',
'forms.delete_field' : 'فیلد را حذف کنید؟',
'forms.delete_confirm': 'سند حذف شود؟',
'forms.revert': 'بازگردانی',
'forms.confirm_discard': 'تغییرات لغو شوند؟',
'forms.day': 'روز',
'forms.month': 'ماه',
'forms.year': 'سال',
'forms.start_adornment_url_icon': 'آیکون اینترنت',
'forms.start_adornment_email_icon': 'نماد ایمیل',
'forms.start_adornment_social_icon': '',
'users.profile': 'مشخصات',
'users.complete_profile': 'مشخصات خود را تکمیل فرمایید.',
'users.profile_completed': 'مشخصات تکمیل شد.',
'users.edit_account': 'ویرایش حساب کاربری',
'users.edit_success': 'کاربر “{name}” ویرایش شد',
'users.log_in': 'ورود',
'users.sign_up': 'خروج',
'users.sign_up_log_in': 'ثبت نام/ورود',
'users.log_out': 'خروج',
'users.bio': 'زندگی نامه',
'users.displayName': 'نام نمایشی',
'users.email': 'ایمیل',
'users.twitterUsername': 'نام کاربری توییتر',
'users.website': 'وبسایت',
'users.groups': 'گروه ها',
'users.avatar': 'آواتار',
'users.notifications': 'اطلاعیه ها',
'users.notifications_users': 'اطلاعیه های کاربران جدید',
'users.notifications_posts': 'اطلاعیه های پست های جدید',
'users.newsletter_subscribeToNewsletter': 'عضویت در خبرنامه',
'users.users_admin': 'مدیر',
'users.admin': 'مدیر',
'users.host': '???',
'users.isAdmin': 'مدیر',
'users.posts': 'پست ها',
'users.upvoted_posts': 'پست هایی که بیشتر پسند شده اند',
'users.please_log_in': 'لطفا وارد شوید',
'users.please_sign_up_log_in': 'لطفا ثبت نام کنید یا وارد شوید.',
'users.cannot_post': 'متاسفانه شما اکنون دسترسی پست کردن ندارید.',
'users.cannot_comment': 'متاسفانه شما اکنون دسترسی ارسال نظر ندارید.',
'users.subscribe': 'اشتراک در پست های این کاربر',
'users.unsubscribe': 'لغو اشتراک از پست های این کاربر',
'users.subscribed': 'شما مشترک “{name}” پست ها شدید.',
'users.unsubscribed': 'شما اشتراکتان را از “{name}” پست ها غیرفعال کردید.',
'users.subscribers': 'مشترکان',
'users.delete': 'حذف کاربر',
'users.delete_confirm': 'کاربر حذف شود؟',
'users.email_already_taken': 'این ایمیل قبلا ثبت شده است: {value}',
settings: 'تنظیمات',
'settings.json_message':
'توجه: تنظیمات ارایه شده در settings.json غیرفعال خواهد شد.',
'settings.edit': 'ویرایش تنظیمات',
'settings.edited': 'تنظیمات ویرایش شد. (لطفا ریلود کنید)',
'settings.title': 'عنوان',
'settings.siteUrl': 'آدرس سایت',
'settings.tagline': 'تگلاین',
'settings.description': 'توضیحات',
'settings.siteImage': 'تصویر سایت',
'settings.defaultEmail': 'ایمیل پیشفرض',
'settings.mailUrl': 'آدرس ایمیل',
'settings.scoreUpdate': 'بروزرسانی امتیاز',
'settings.postInterval': 'فاصله پست کردن',
'settings.RSSLinksPointTo': 'RSS Links Point To',
'settings.commentInterval': 'فاصله نظر گذاشتن',
'settings.maxPostsPerDay': 'حداکثر تعداد پست در روز',
'settings.startInvitesCount': 'شروع شمارش دعوت ها',
'settings.postsPerPage': 'تعداد پست ها در صفحه',
'settings.logoUrl': 'آدرس لوگو',
'settings.logoHeight': 'ارتفاع لوگو',
'settings.logoWidth': 'عرض لوگو',
'settings.faviconUrl': 'آدرس فاویکون',
'settings.twitterAccount': 'حساب توییتر',
'settings.facebookPage': 'صفحه فیسبوک',
'settings.googleAnalyticsId': 'Google Analytics ID',
'settings.locale': 'بومی',
'settings.requireViewInvite': 'دعپت به مشاهده نیاز است',
'settings.requirePostInvite': 'دعوت به پست کردن نیاز است',
'settings.requirePostsApproval': 'احتیاج به تایید پست ها',
'settings.scoreUpdateInterval': 'فاصله بروزرسانی امتیاز ها',
'app.loading': 'درحال بارگذاری...',
'app.404': 'متاسفانه چیزی که دنبال آن بودید یافت نشد.',
'app.missing_document': 'متاسفانه سند درخواست شده یافت نشد.',
'app.powered_by': 'ساخته شده با Vulcan.js',
'app.or': 'یا',
'app.noPermission': 'متاسفانه شما اکنون دسترسی به انجام اینکار را ندارید.',
'app.operation_not_allowed':
'متاسفانه شما اجازه اجرای این درخواست را ندارید: "{operationName}"',
'app.document_not_found': 'سند یافت نشد (شناسه: {value})',
'app.disallowed_property_detected': 'Disallowed property detected: {value}',
'app.something_bad_happened': 'اتفاق بدی افتاد ...',
'app.embedly_not_authorized':
'Invalid Embedly API key provided in the settings file. To find your key, log into https://app.embed.ly -> API',
'app.defaultError': '{defaultMessage}',
'app.please_sign_up_log_in': 'Please sign up or log in',
'app.no_access_permissions': 'Sorry, you are not allowed to access this page.',
'cards.edit': 'ویرایش',
'datatable.new': 'جدید',
'datatable.edit': 'ویرایش',
'datatable.search': 'جستجو',
admin: 'مدیر',
notifications: 'اطلاعیه ها',
'errors.expectedType':
'Expected type {dataType} for field “{label}”, received “{value}” instead.',
'errors.required': 'فیلد “{label}” الزامی است.',
'errors.minString': 'فیلد “{label}” باید حداقل {min} کاراکتر داشته باشد',
'errors.maxString': 'فیلد “{label}” باید حداگثر {max} کاراکتر داشته باشد.',
'errors.generic': 'متاسفانه خطایی پیش آمد: {errorMessage}.',
'errors.generic_report':
'متاسفانه خطایی پیش آمد: {errorMessage}. گزارش خطا ایجاد شد.',
'errors.minNumber': 'فیلد “{label}” باید بیشتر باشد از {min}. ',
'errors.maxNumber': 'فیلد “{label}” باید کمتر باشد از {max}. ',
'errors.minCount': 'باید حداقل {count} از فیلد وجود داشته باشد “{label}”.',
'errors.maxCount': 'فیلد “{label}” فقظ به تعداد {count} مجاز است.',
'errors.regEx': 'فیلد “{label}”: wrong formatting',
'errors.badDate': 'فیلد “{label}” تاریخ نیست.',
'errors.notAllowed': 'مقدار فیلد “{label}” قابل قبول نیست.',
'errors.noDecimal': 'مقدار فیلد “{label}” نباید اعشاری باشد.',
//TODO other simple schema errors
'errors.minNumberExclusive': '',
'errors.maxNumberExclusive': '',
'errors.keyNotInSchema': '',
});
================================================
FILE: packages/vulcan-i18n-fa-ir/package.js
================================================
Package.describe({
name: 'vulcan:i18n-fa-ir',
summary: 'Vulcan i18n package (fa_IR)',
version: '1.16.9',
git: 'https://github.com/VulcanJS/Vulcan.git',
});
Package.onUse(function(api) {
api.use(['vulcan:core@=1.16.9']);
api.addFiles(['lib/fa_IR.js'], ['client', 'server']);
});
================================================
FILE: packages/vulcan-i18n-fr-fr/README.md
================================================
Vulcan i18n fr_FR package.
================================================
FILE: packages/vulcan-i18n-fr-fr/lib/fr_FR.js
================================================
import { addStrings } from 'meteor/vulcan:core';
addStrings('fr', {
'accounts.error_incorrect_password': 'Mot de passe invalide',
'accounts.error_email_required': 'Email requis',
'accounts.error_email_already_exists': 'Email déjà utilisé',
'accounts.error_invalid_email': 'Email invalide',
'accounts.error_minchar': 'Votre mot de passe est trop court',
'accounts.error_username_required': 'Nom d\'utilisateur requis',
'accounts.error_accounts_': '',
'accounts.error_unknown': 'Erreur inconnue',
'accounts.error_user_not_found': 'Utilisateur inconnu',
'accounts.error_username_already_exists': 'Nom d\'utilisateur déjà utilisé',
'accounts.enter_username_or_email': 'Nom d\'utilisateur ou email',
'accounts.error_internal_server_error': 'Erreur serveur interne',
'accounts.error_token_expired': 'Erreur: token invalide',
'accounts.username_or_email': 'Nom d\'utilisateur ou email',
'accounts.enter_username': 'Nom d\'utilisateur',
'accounts.username': 'Nom d\'utilisateur',
'accounts.enter_email': 'Email',
'accounts.email': 'Email',
'accounts.enter_password': 'Mot de passe',
'accounts.password': 'Mot de passe',
'accounts.choose_password': 'Choisir un mot de passe',
'accounts.change_password': 'Changer le mot de passe',
'accounts.reset_your_password': 'Réinitialiser le mot de passe',
'accounts.set_password': 'Définir le mot de passe',
'accounts.enter_new_password': 'Entrez un nouveau mot de passe',
'accounts.new_password': 'Nouveau mot de passe',
'accounts.forgot_password': 'Mot de passe oublié',
'accounts.sign_up': 'Inscription',
'accounts.sign_in': 'Connexion',
'accounts.sign_out': 'Se déconnecter',
'accounts.cancel': 'Annuler',
'accounts.or_use': 'ou utilisez',
'accounts.info_email_sent': 'Email envoyé.',
'accounts.info_password_changed': 'Mot de passe changé.',
'accounts.logging_in': 'Connexion en cours…',
'accounts.email_verified': 'Votre adresse e-mail a été vérifiée.',
'forms.submit': 'Envoyer',
'forms.cancel': 'Annuler',
'forms.select_option': '-- Choisir une option --',
'forms.add_nested_field': 'Ajouter un {label}',
'forms.delete_nested_field': 'Supprimer ce {label} ?',
'forms.delete': 'Supprimer',
'forms.delete_field': 'Supprimer le champ ?',
'forms.delete_confirm': 'Supprimer le document ?',
'forms.next': 'Suivant',
'forms.previous': 'Précédent',
'forms.revert': 'Retour',
'forms.confirm_discard': 'Supprimer les modifications ?',
'forms.start_adornment_url_icon': 'Icône de internet',
'forms.start_adornment_email_icon': 'Icône de courriel',
'forms.start_adornment_social_icon': '',
'users.profile': 'Profil',
'users.complete_profile': 'Complétez votre profil',
'users.profile_completed': 'Profil completé.',
'users.edit_account': 'Modifier le compte',
'users.edit_success': 'Utilisateur “{name}” modifié',
'users.log_in': 'Se connecter',
'users.sign_up': 'S\'inscrire',
'users.sign_up_log_in': 'Inscription / Connexion',
'users.log_out': 'Se déconnecter',
'users.bio': 'Bio',
'users.displayName': 'Nom d\'affichage',
'users.email': 'Email',
'users.twitterUsername': 'Pseudo Twitter',
'users.website': 'Website',
'users.groups': 'Groupes',
'users.avatar': 'Avatar',
'users.notifications': 'Notifications',
'users.notifications_users': 'Notifications de nouvel utilisateur',
'users.notifications_posts': 'Notifications de nouveau post',
'users.newsletter_subscribeToNewsletter': 'S\'inscrire à la newsletter',
'users.users_admin': 'Admin',
'users.admin': 'Admin',
'users.host': 'Membre de l\'équipe',
'users.isAdmin': 'Administrateur',
'users.posts': 'Posts',
'users.upvoted_posts': 'Posts soutenus',
'users.please_log_in': 'Connectez-vous',
'users.please_sign_up_log_in': 'Connectez-vous ou inscrivez-vous',
'users.cannot_post': 'Désolé, vous n\'avez pas la permission de publier pour le moment',
'users.cannot_comment': 'Désolé, vous n\'avez pas la permission de commenter pour le moment',
'users.subscribe': 'S\'inscrire aux posts de cet utilisateur',
'users.unsubscribe': 'Se désinscrire des posts de cet utilisateur',
'users.subscribed': 'Vous êtes abonné aux posts de “{name}”.',
'users.unsubscribed': 'Vous n\'êtes plus abonné aux posts de “{name}”.',
'users.subscribers': 'Abonnés',
'users.delete': 'Supprimer l\'utilistateur',
'users.delete_confirm': 'Supprimer cet utilisateur?',
'users.email_already_taken': 'Email déjà pris: {value}',
'settings': 'Paramètres',
'settings.json_message': 'Note: les paramètres déjà renseignés dans le fichier settings.json seront désactivés.',
'settings.edit': 'Modifier les paramètres',
'settings.edited': 'Paramètres modifiés (recharger).',
'settings.title': 'Titre',
'settings.siteUrl': 'URL du site',
'settings.tagline': 'Tagline',
'settings.description': 'Description',
'settings.siteImage': 'Image du site',
'settings.defaultEmail': 'Email par défaut',
'settings.mailUrl': 'URL du mail',
'settings.scoreUpdate': 'Rafraichissement du score',
'settings.postInterval': 'Intervalle de publication',
'settings.RSSLinksPointTo': 'Liens RSS pointent vers',
'settings.commentInterval': 'Intervalle de commentaires',
'settings.maxPostsPerDay': 'Posts quotidiens maximum',
'settings.startInvitesCount': 'Démarrer le compte d\'invitations',
'settings.postsPerPage': 'Posts par page',
'settings.logoUrl': 'URL du logo',
'settings.logoHeight': 'Hauteur du logo',
'settings.logoWidth': 'Largeur du logo',
'settings.faviconUrl': 'URL du favicon',
'settings.twitterAccount': 'Compte Twitter',
'settings.facebookPage': 'Page Facebook',
'settings.googleAnalyticsId': 'ID Google Analytics',
'settings.locale': 'Locale',
'settings.requireViewInvite': 'Nécessite une invitation pour voir',
'settings.requirePostInvite': 'Nécessite une invitation pour publier',
'settings.requirePostsApproval': 'Nécessite l\'approbation des posts',
'settings.scoreUpdateInterval': 'Intervalle de mise à jour du score',
'app.loading': 'Chargement…',
'app.404': 'Désolé, ce contenu n\'est pas disponible.',
'app.missing_document': 'Désolé, nous n\'avons pas trouvé le document que vous cherchiez',
'app.powered_by': 'Construit avec Vulcan.js',
'app.or': 'Ou',
'app.noPermission': 'Désolé, vous n\'êtes pas autorisé à faire cette action pour le moment',
'app.operation_not_allowed': 'Désolé, vous n\'avez pas les droits pour faire l\'opération "{operationName}"',
'app.document_not_found': 'Document introuvable: (id: {value})',
'app.disallowed_property_detected': 'Propriété refusée détectée: {value}',
'app.something_bad_happened': 'Quelque chose s\'est mal passé...',
'app.embedly_not_authorized': 'Clé d\'API Embedly invalide renseignée dans les paramètres. Pour trouver votre clé, connectez-vous sur: https://app.embed.ly -> API',
'app.defaultError': '{defaultMessage}',
'app.please_sign_up_log_in': 'Please sign up or log in',
'app.no_access_permissions': 'Sorry, you are not allowed to access this page.',
'cards.edit': 'Modifier',
'datatable.new': 'Nouveau',
'datatable.edit': 'Modifier',
'datatable.search': 'Rechercher',
'admin': 'Admin',
'notifications': 'Notifications',
'errors.expectedType': 'Un champ “{label}” de type {dataType} était attendu, “{value}” a été reçu à la place.',
'errors.required': 'Le champ “{label}” est requis.',
'errors.minString': 'Le champ "{label}" doit faire au moins {min} caractères.',
'errors.maxString': 'Le champ “{label}” est limité à {max} caractères.',
'errors.generic':'Désolé, une erreur est survenue: {errorMessage}',
'errors.generic_report':'Désolé, une erreur est survenue: {errorMessage}. Un message d\'erreur a été envoyé.',
'errors.minNumber':'Le champ “{label}” doit être supérieur à {min}.',
'errors.maxNumber':'Le champ “{label}” doit être inférieur à {max}.',
'errors.minCount':'Il faut au moins {count} objets dans le champ “{label}”.',
'errors.maxCount':'Le champ “{label}” est limité à {count} objets',
'errors.regEx':'Le champ “{label}” est mal formatté',
'errors.badDate':'Le champ “{label}” n\'est pas une date',
'errors.notAllowed':'La valeur du champ "{label}" est interdite.',
'errors.noDecimal':'La valeur du champ "{label}" ne peut être décimale.',
'errors.minNumberExclusive':'',
'errors.maxNumberExclusive':'',
'errors.keyNotInSchema':'',
});
================================================
FILE: packages/vulcan-i18n-fr-fr/package.js
================================================
Package.describe({
name: 'vulcan:i18n-fr-fr',
summary: 'Vulcan i18n package (fr_FR)',
version: '1.16.9',
git: 'https://github.com/VulcanJS/Vulcan.git',
});
Package.onUse(function(api) {
api.use(['vulcan:core@=1.16.9']);
api.addFiles(['lib/fr_FR.js'], ['client', 'server']);
});
================================================
FILE: packages/vulcan-lib/README.md
================================================
Vulcan libraries package, used internally.
================================================
FILE: packages/vulcan-lib/lib/client/apollo-client/apolloClient.js
================================================
import { ApolloClient } from '@apollo/client';
import { ApolloLink } from 'apollo-link';
import httpLink from './links/http';
import meteorAccountsLink from './links/meteor';
import errorLink from './links/error';
// import { createStateLink } from '../../modules/apollo-common/links/state.js';
import { resetReactiveState } from '../../modules/reactive-state.js';
import createCache from './cache';
import { getTerminatingLinks, getLinks } from './links/registerLinks';
// these links do not change once created
const staticLinks = [errorLink, meteorAccountsLink];
let apolloClient;
export const createApolloClient = () => {
// links registered by packages
const cache = createCache();
const registeredLinks = getLinks();
const terminatingLinks = getTerminatingLinks();
if (terminatingLinks.length > 1) console.warn('Warning: You registered more than one terminating Apollo link.');
// const stateLink = createStateLink({ cache });
const newClient = new ApolloClient({
link: ApolloLink.from([
// stateLink,
...registeredLinks,
...staticLinks,
// terminating
...(terminatingLinks.length ? terminatingLinks : [httpLink]),
]),
cache,
});
resetReactiveState();
newClient.onResetStore(resetReactiveState);
// register the client
apolloClient = newClient;
return newClient;
};
export const getApolloClient = () => {
if (!apolloClient) {
// eslint-disable-next-line no-console
console.warn('Warning: accessing apollo client before it is initialized.');
}
return apolloClient;
};
// This is a draft of what could be a reload of the apollo client with new Links
// for the moment there seems to be no equivalent to Redux `replaceReducers` in apollo-client
//@see https://github.com/apollographql/apollo-link-state/issues/306
//export const reloadApolloClient = () => {
// // get the current cache
// const currentCache = apolloClient.cache;
// // get the stateLink
// const newApolloClient = createApolloClient({
// link: ApolloLink.from([getStateLink(), ...staticLinks]),
// cache: currentCache
// });
// // update the client
// apolloClient = newApolloClient;
// return newApolloClient;
//};
================================================
FILE: packages/vulcan-lib/lib/client/apollo-client/cache.js
================================================
import { InMemoryCache } from '@apollo/client';
import { getFragmentMatcher } from '../../modules/fragment_matcher';
const createCache = () => new InMemoryCache({ fragmentMatcher: getFragmentMatcher() })
//ssr
.restore(window.__APOLLO_STATE__);
export default createCache;
================================================
FILE: packages/vulcan-lib/lib/client/apollo-client/index.js
================================================
export * from './apolloClient';
export * from './links/registerLinks';
================================================
FILE: packages/vulcan-lib/lib/client/apollo-client/links/error.js
================================================
import { onError } from '@apollo/client/link/error';
const locationsToStr = (locations=[]) => locations.map(({column, line}) => `line ${line}, col ${column}`).join(';');
const errorLink = onError(error => {
const { graphQLErrors, networkError } = error;
if (graphQLErrors)
graphQLErrors.map(({ message, locations, path }) => {
// eslint-disable-next-line no-console
console.log(`[GraphQL error]: Message: ${message}, Location: ${locationsToStr(locations)}, Path: ${path}`);
});
if (networkError) {
// eslint-disable-next-line no-console
console.log(`[${networkError.statusCode} ${networkError.response?.statusText}]: ${networkError.message}`);
}
});
export default errorLink;
================================================
FILE: packages/vulcan-lib/lib/client/apollo-client/links/http.js
================================================
import { HttpLink } from '@apollo/client';
const httpLink = new HttpLink({
uri: '/graphql',
credentials: 'same-origin',
});
export default httpLink;
================================================
FILE: packages/vulcan-lib/lib/client/apollo-client/links/meteor.js
================================================
import { MeteorAccountsLink } from 'meteor/apollo';
const meteorAccountsLink = new MeteorAccountsLink();
export default meteorAccountsLink;
================================================
FILE: packages/vulcan-lib/lib/client/apollo-client/links/registerLinks.js
================================================
const terminatingLinksRegistry = [];
const linksRegistry = [];
// register one or more links
export const registerLink = (link) => {
const links = Array.isArray(link) ? link : [link];
linksRegistry.unshift(...links);
};
export const registerTerminatingLink = (link) => {
const links = Array.isArray(link) ? link : [link];
terminatingLinksRegistry.push(...links);
};
export const getLinks = () => linksRegistry;
export const getTerminatingLinks = () => terminatingLinksRegistry;
================================================
FILE: packages/vulcan-lib/lib/client/auth.js
================================================
/**
* Manage meteor_login_token cookie
* Necessary for authentication when the
* Authorization header is not set
*
* E.g on first page loading
*/
import Cookies from 'universal-cookie';
import { Meteor } from 'meteor/meteor';
const cookie = new Cookies();
function setToken(loginToken, expires) {
if (loginToken && expires !== -1) {
cookie.set('meteor_login_token', loginToken, {
path: '/',
expires,
sameSite: 'lax',
secure: document.domain !== 'localhost',
});
} else {
cookie.remove('meteor_login_token', {
path: '/',
});
}
}
function initToken() {
const loginToken = global.localStorage['Meteor.loginToken'];
const loginTokenExpires = new Date(global.localStorage['Meteor.loginTokenExpires']);
if (loginToken) {
setToken(loginToken, loginTokenExpires);
} else {
setToken(null, -1);
}
}
Meteor.startup(() => {
initToken();
});
// TODO: cleanup
// This part of the code overrides the default localStorage function,
// so that when Meteor.loginToken is set, it is also automatically
// stored as a cookie (necessary for SSR to work as expected for all HTTP requests)
const originalSetItem = Meteor._localStorage.setItem;
Meteor._localStorage.setItem = function setItem(key, value) {
if (key === 'Meteor.loginToken') {
Meteor.defer(initToken);
}
originalSetItem.call(Meteor._localStorage, key, value);
};
const originalRemoveItem = Meteor._localStorage.removeItem;
Meteor._localStorage.removeItem = function removeItem(key) {
if (key === 'Meteor.loginToken') {
Meteor.defer(initToken);
}
originalRemoveItem.call(Meteor._localStorage, key);
};
================================================
FILE: packages/vulcan-lib/lib/client/connectors.js
================================================
// Mock exports so resolver/mutation build doesn't fail client side
export const DatabaseConnectors = null;
export const Connectors = null;
================================================
FILE: packages/vulcan-lib/lib/client/errors.js
================================================
// mock apollo server errors
export const throwError = (error) => { if (error) throw new Error(error.id, error); };
================================================
FILE: packages/vulcan-lib/lib/client/inject_data.js
================================================
import { EJSON } from 'meteor/ejson';
import { onPageLoad } from 'meteor/server-render';
// InjectData object
export const InjectData = {
// data object
_data: {},
_ready: false,
// encode object to string
_encode(ejson) {
const ejsonString = EJSON.stringify(ejson);
return encodeURIComponent(ejsonString);
},
// decode string to object
_decode(encodedEjson) {
const decodedEjsonString = decodeURIComponent(encodedEjson);
if (!decodedEjsonString) return null;
return EJSON.parse(decodedEjsonString);
},
_checkReady() {
if (!this._ready) {
const dom = document.querySelector('script[type="text/inject-data"]');
const injectedDataString = dom ? dom.textContent.trim() : '';
this._data = InjectData._decode(injectedDataString) || {};
this._ready = true;
}
},
// sync version
// Must always be called inside an onPageLoad callback
getDataSync(key) {
this._checkReady();
return this._data[key];
},
// get data when DOM loaded
getData(key, callback) {
// promisified version
if (!callback) {
return new Promise((resolve, reject) => {
onPageLoad(() => {
this._checkReady();
resolve(this._data[key]);
});
});
}
onPageLoad(() => {
this._checkReady();
callback(this._data[key]);
});
},
};
================================================
FILE: packages/vulcan-lib/lib/client/main.js
================================================
import './auth.js';
export * from '../modules/index.js';
export * from './inject_data.js';
export * from './apollo-client';
// createCollection, resolvers and mutations mocks
// avoid warnings when building with webpack
export * from './connectors';
export * from './mock';
export * from './errors';
================================================
FILE: packages/vulcan-lib/lib/client/mock.js
================================================
// mock mutators
export const createMutator = null;
export const updateMutator = null;
export const deleteMutator = null;
// mock default mutations and resolvers
export const getDefaultResolvers = () => ({});
export const getDefaultMutations = () => ({});
================================================
FILE: packages/vulcan-lib/lib/modules/admin.js
================================================
export let AdminColumns = [];
export const addAdminColumn = columnOrColumns => {
if (Array.isArray(columnOrColumns)) {
AdminColumns = AdminColumns.concat(columnOrColumns);
} else {
AdminColumns.push(columnOrColumns);
}
};
================================================
FILE: packages/vulcan-lib/lib/modules/apollo-common/index.js
================================================
export * from './links/state';
import './settings';
================================================
FILE: packages/vulcan-lib/lib/modules/apollo-common/links/state.js
================================================
/**
* Setup apollo-link-state
* Apollo-link-state helps to manage a local store for caching and client-side
* data storing
* It replaces previous implementation using redux
* Link state doc:
* @see https://www.apollographql.com/docs/react/essentials/local-state.html
* @see https://www.apollographql.com/docs/link/links/state.html
* General presentation on Links
* @see https://www.apollographql.com/docs/link/
* Example
* @see https://hackernoon.com/storing-local-state-in-react-with-apollo-link-state-738f6ca45569
*/
import { withClientState } from 'apollo-link-state';
/**
* Create a state link
* TODO: Deprecated
*/
export const createStateLink = ({ cache, resolvers, defaults, ...otherOptions }) => {
const stateLink = withClientState({
cache,
defaults: defaults || getStateLinkDefaults(),
resolvers: resolvers || getStateLinkResolvers(),
...otherOptions,
});
return stateLink;
};
// enhancement workflow
const registeredDefaults = {};
/**
* Defaults are default response to queries
*/
export const registerStateLinkDefault = ({ name, defaultValue, options = {} }) => {
registeredDefaults[name] = defaultValue;
return registeredDefaults;
};
export const getStateLinkDefaults = () => registeredDefaults;
// Mutation are equivalent to a Redux Action + Reducer
// except it uses GraphQL to retrieve/update data in the cache
const registeredMutations = {};
export const registerStateLinkMutation = ({ name, mutation, options = {} }) => {
registeredMutations[name] = mutation;
return registeredMutations;
};
export const getStateLinkMutations = () => registeredMutations;
export const getStateLinkResolvers = () => ({
Mutation: getStateLinkMutations(),
});
================================================
FILE: packages/vulcan-lib/lib/modules/apollo-common/settings.js
================================================
import { registerSetting } from '../settings';
registerSetting('apolloSsr.disable', false, 'Disable Server Side Rendering');
================================================
FILE: packages/vulcan-lib/lib/modules/callbacks.js
================================================
import { Meteor } from 'meteor/meteor';
import { debug } from './debug.js';
import { Utils } from './utils';
import merge from 'lodash/merge';
/**
* @summary Format callback hook names
*/
export const formatHookName = hook => typeof hook === 'string' && hook.toLowerCase();
/**
* @summary A list of all registered callback hooks
*/
export const CallbackHooks = [];
/**
* @summary Callback hooks provide an easy way to add extra steps to common operations.
* @namespace Callbacks
*/
export const Callbacks = {};
/**
* @summary Register a callback
* @param {String} hook - The name of the hook
* @param {Function} callback - The callback function
*/
export const registerCallback = function (callback) {
CallbackHooks.push(callback);
};
/**
* @summary Add a callback function to a hook
* @param {String} hook - The name of the hook
* @param {Function} callback - The callback function
*/
export const addCallback = function (hook, callback) {
const formattedHook = formatHookName(hook);
if (!callback.name) {
// eslint-disable-next-line no-console
console.log(`// Warning! You are adding an unnamed callback to ${formattedHook}. Please use the function foo () {} syntax.`);
}
// if callback array doesn't exist yet, initialize it
if (typeof Callbacks[formattedHook] === 'undefined') {
Callbacks[formattedHook] = [];
}
Callbacks[formattedHook].push(callback);
};
/**
* @summary Remove a callback from a hook
* @param {string} hookName - The name of the hook
* @param {string} callbackName - The name of the function to remove
*/
export const removeCallback = function (hookName, callbackName) {
const formattedHook = formatHookName(hookName);
Callbacks[formattedHook] = _.reject(Callbacks[formattedHook], function (callback) {
return callback.name === callbackName;
});
};
/**
* @summary Remove all callbacks from a hook (mostly for testing purposes)
* @param {string} hookName - The name of the hook
*/
export const removeAllCallbacks = function(hookName) {
const formattedHook = formatHookName(hookName);
Callbacks[formattedHook] = [];
};
/**
* @summary Successively run all of a hook's callbacks on an item
* @param {String} hook - First argument: the name of the hook, or an array
* @param {Object} item - Second argument: the post, comment, modifier, etc. on which to run the callbacks
* @param {Any} args - Other arguments will be passed to each successive iteration
* @param {Array} callbacks - Optionally, pass an array of callback functions instead of passing a hook name
* @returns {Object} Returns the item after it's been through all the callbacks for this hook
*/
export const runCallbacks = function () {
let hook, item, args, callbacks, formattedHook;
if (typeof arguments[0] === 'object' && arguments.length === 1) {
const singleArgument = arguments[0];
hook = singleArgument.name;
formattedHook = formatHookName(hook);
item = singleArgument.iterator;
args = singleArgument.properties;
// if callbacks option is passed used that, else use formatted hook name
callbacks = singleArgument.callbacks ? singleArgument.callbacks : Callbacks[formattedHook];
} else {
// OpenCRUD backwards compatibility
// the first argument is the name of the hook or an array of functions
hook = arguments[0];
formattedHook = formatHookName(hook);
// the second argument is the item on which to iterate
item = arguments[1];
// successive arguments are passed to each iteration
args = Array.prototype.slice.call(arguments).slice(2);
// if first argument is an array, use that as callbacks array; else use formatted hook name
callbacks = Array.isArray(hook) ? hook : Callbacks[formattedHook];
}
// flag used to detect the callback that initiated the async context
let asyncContext = false;
if (typeof callbacks !== 'undefined' && callbacks.length > 0) { // if the hook exists, and contains callbacks to run
const runCallback = (accumulator, callback) => {
debug(`\x1b[32m>> Running callback [${callback.name}] on hook [${formattedHook}]\x1b[0m`);
const newArguments = [accumulator].concat(args);
try {
const result = callback.apply(this, newArguments);
// if callback is only supposed to run once, remove it
if (callback.runOnce) {
removeCallback(formattedHook, callback.name);
}
if (typeof result === 'undefined') {
// if result of current iteration is undefined, don't pass it on
// debug(`// Warning: Sync callback [${callback.name}] in hook [${hook}] didn't return a result!`)
return accumulator;
} else {
return result;
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(`\x1b[31m// error at callback [${callback.name}] in hook [${formattedHook}]\x1b[0m`);
// eslint-disable-next-line no-console
console.log(error);
if (error.break || error.data && error.data.break) {
throw error;
}
// pass the unchanged accumulator to the next iteration of the loop
return accumulator;
}
};
return callbacks.reduce(function (accumulator, callback, index) {
if (Utils.isPromise(accumulator)) {
if (!asyncContext) {
debug(`\x1b[32m>> Started async context in hook [${formattedHook}] by [${callbacks[index-1] && callbacks[index-1].name}]\x1b[0m`);
asyncContext = true;
}
return new Promise((resolve, reject) => {
accumulator
.then(result => {
try {
// run this callback once we have the previous value
resolve(runCallback(result, callback));
} catch (error) {
// error will be thrown only for breaking errors, so throw it up in the promise chain
reject(error);
}
})
.catch(reject);
});
} else {
return runCallback(accumulator, callback);
}
}, item);
} else { // else, just return the item unchanged
return item;
}
};
/**
* @summary Successively run all of a hook's callbacks on an item, in async mode (only works on server)
* @param {String} hook - First argument: the name of the hook
* @param {Any} args - Other arguments will be passed to each successive iteration
*/
export const runCallbacksAsync = function() {
let hook, args, callbacks, formattedHook;
if (typeof arguments[0] === 'object' && arguments.length === 1) {
const singleArgument = arguments[0];
hook = singleArgument.name;
formattedHook = formatHookName(hook);
args = [singleArgument.properties]; // wrap in array for apply
callbacks = singleArgument.callbacks ? singleArgument.callbacks : Callbacks[formattedHook];
} else {
// OpenCRUD backwards compatibility
// the first argument is the name of the hook or an array of functions
hook = arguments[0];
formattedHook = formatHookName(hook);
callbacks = Array.isArray(hook) ? hook : Callbacks[formattedHook];
// successive arguments are passed to each iteration
args = Array.prototype.slice.call(arguments).slice(1);
// if first argument is an array, use that as callbacks array; else use formatted hook name
callbacks = Array.isArray(hook) ? hook : Callbacks[formattedHook];
}
if (typeof callbacks !== 'undefined' && !!callbacks.length) {
const _runCallbacksAsync = () =>
Promise.all(
callbacks.map(callback => {
if (!callback) {
throw new Error(`Found undefined callback on hook ${hook}`);
}
debug(`\x1b[32m>> Running async callback [${callback.name}] on hook [${hook}]\x1b[0m`);
return callback.apply(this, args);
}),
);
if (Meteor.isServer) {
// TODO: find out if we can safely use promises on the server, too - https://github.com/VulcanJS/Vulcan/pull/2065
return new Promise(async (resolve, reject) => {
Meteor.defer(function() {
_runCallbacksAsync().then(resolve).catch(reject);
});
});
}
return _runCallbacksAsync();
}
return [];
};
export let globalCallbacks = {
create: {
validate: [],
before: [],
after: [],
async: [],
},
update: {
validate: [],
before: [],
after: [],
async: [],
},
delete: {
validate: [],
before: [],
after: [],
async: [],
}
};
export const addGlobalCallbacks = callbacks => {
globalCallbacks = merge(globalCallbacks, callbacks);
};
================================================
FILE: packages/vulcan-lib/lib/modules/collections.js
================================================
import { Mongo } from 'meteor/mongo';
import SimpleSchema from 'simpl-schema';
import { Utils } from './utils.js';
import { runCallbacks, runCallbacksAsync, registerCallback, addCallback } from './callbacks.js';
import { getSetting, registerSetting } from './settings.js';
import { registerFragment } from './fragments.js';
import { getDefaultFragmentText } from './graphql/defaultFragment';
import escapeStringRegexp from 'escape-string-regexp';
import { validateIntlField, getIntlString, isIntlField, schemaHasIntlFields, schemaHasIntlField } from './intl';
import clone from 'lodash/clone';
import isEmpty from 'lodash/isEmpty';
import merge from 'lodash/merge';
import _omit from 'lodash/omit';
import mergeWith from 'lodash/mergeWith';
import { createSchema, isCollectionType } from './schema_utils.js';
const wrapAsync = Meteor.wrapAsync ? Meteor.wrapAsync : Meteor._wrapAsync;
// import { debug } from './debug.js';
registerSetting('maxDocumentsPerRequest', 1000, 'Maximum documents per request');
// will be set to `true` if there is one or more intl schema fields
export let hasIntlFields = false;
export const Collections = [];
export const getCollection = name => {
const collection = Collections.find(
({ options: { collectionName } }) => name === collectionName || name === collectionName.toLowerCase()
);
if (!collection) {
throw new Error(`Could not find collection named “${name}”`);
}
return collection;
};
export const getCollectionByTypeName = typeName => {
// in case typeName is for an array ('[User!]'), get rid of brackets
let parsedTypeName = typeName.replace('[', '').replace(']', '').replace(/!/g, '');
const collection = Collections.find(({ options: { typeName } }) => parsedTypeName === typeName);
if (!collection) {
throw new Error(`Could not find collection for type “${parsedTypeName}”. Registered types: ${Collections.map(({ options: { typeName } }) => typeName).join(', ')}`);
}
return collection;
};
export const generateCollectionNameFromTypeName = typeName => Utils.pluralize(typeName);
export const getTypeNameByCollectionName = collectionName => {
const collection = Collections.find(({ options }) => options.collectionName === collectionName);
if (!collection) {
throw new Error(`Could not find type for collection “${collectionName}”`);
}
return collection.options.typeName;
};
export const generateTypeNameFromCollectionName = collectionName => Utils.singularize(collectionName);
/**
* @summary replacement for Collection2's attachSchema. Pass either a schema, to
* initialize or replace the schema, or some fields, to extend the current schema
* @class Mongo.Collection
*/
Mongo.Collection.prototype.attachSchema = function (schemaOrFields) {
if (schemaOrFields instanceof SimpleSchema) {
this.simpleSchema = () => schemaOrFields;
} else {
this.simpleSchema().extend(schemaOrFields);
}
};
/**
* @summary Add an additional field (or an array of fields) to a schema.
* @param {Object|Object[]} fieldOrFieldArray
*/
Mongo.Collection.prototype.addField = function (fieldOrFieldArray) {
const collection = this;
const fieldSchema = {};
const fieldArray = Array.isArray(fieldOrFieldArray) ? fieldOrFieldArray : [fieldOrFieldArray];
// loop over fields and add them to schema (or extend existing fields)
fieldArray.forEach(function (field) {
fieldSchema[field.fieldName] = field.fieldSchema;
});
// add field schema to collection schema
collection.attachSchema(createSchema(merge(collection.options.schema, fieldSchema)));
};
/**
* @summary Remove a field from a schema.
* @param {String} fieldName
*/
Mongo.Collection.prototype.removeField = function (fieldName) {
var collection = this;
var schema = _omit(collection.simpleSchema()._schema, fieldName);
// add field schema to collection schema
collection.attachSchema(createSchema(schema));
};
/**
* @summary Add a default view function.
* @param {Function} view
*/
Mongo.Collection.prototype.addDefaultView = function (view) {
this.defaultView = view;
};
/**
* @summary Add a named view function.
* @param {String} viewName
* @param {Function} view
*/
Mongo.Collection.prototype.addView = function (viewName, view) {
this.views[viewName] = view;
};
/**
* @summary Allow mongodb aggregation
* @param {Array} pipelines mongodb pipeline
* @param {Object} options mongodb option object
*/
Mongo.Collection.prototype.aggregate = function (pipelines, options) {
var coll = this.rawCollection();
return wrapAsync(coll.aggregate.bind(coll))(pipelines, options);
};
// see https://github.com/dburles/meteor-collection-helpers/blob/master/collection-helpers.js
Mongo.Collection.prototype.helpers = function (helpers) {
var self = this;
if (self._transform && !self._helpers)
throw new Meteor.Error(
"Can't apply helpers to '" + self._name + "' a transform function already exists!"
);
if (!self._helpers) {
self._helpers = function Document(doc) {
return Object.assign(this, doc);
};
self._transform = function (doc) {
return new self._helpers(doc);
};
}
Object.keys(helpers).forEach(function (key) {
self._helpers.prototype[key] = helpers[key];
});
};
export const extendCollection = (collection, options) => {
const newOptions = mergeWith({}, collection.options, options, (a, b) => {
if (Array.isArray(a) && Array.isArray(b)) {
return a.concat(b);
}
if (Array.isArray(a) && b) {
return a.concat([b]);
}
if (Array.isArray(b) && a) {
return b.concat([a]);
}
});
collection = createCollection(newOptions);
return collection;
};
/*
Note: this currently isn't used because it would need to be called
after all collections have been initialized, otherwise we can't figure out
if resolved field is resolving to a collection type or not
*/
export const addAutoRelations = () => {
Collections.forEach(collection => {
const schema = collection.simpleSchema()._schema;
// add "auto-relations" to schema resolvers
Object.keys(schema).map(fieldName => {
const field = schema[fieldName];
// if no resolver or relation is provided, try to guess relation and add it to schema
if (field.resolveAs) {
const { resolver, relation, type } = field.resolveAs;
if (isCollectionType(type) && !resolver && !relation) {
field.resolveAs.relation = field.type === Array ? 'hasMany' : 'hasOne';
}
}
});
});
};
/*
Pass an existing collection to overwrite it instead of creating a new one
*/
export const createCollection = (options) => {
const { typeName, collectionName = generateCollectionNameFromTypeName(typeName), dbCollectionName } = options;
let { schema, apiSchema, dbSchema } = options;
const existingCollectionIndex = Collections.findIndex(c => c.collectionName === collectionName);
const existingCollection = existingCollectionIndex >= 0 ? Collections[existingCollectionIndex] : null;
// initialize new Mongo collection or get existing collection when overwriting
const collection =
existingCollection ||
(collectionName === 'Users' && Meteor.users
? Meteor.users
: new Mongo.Collection(dbCollectionName ? dbCollectionName : collectionName.toLowerCase()));
// decorate collection with options
collection.options = options;
// add typeName if missing
collection.typeName = typeName;
collection.options.typeName = typeName;
collection.options.singleResolverName = Utils.camelCaseify(typeName);
collection.options.multiResolverName = Utils.camelCaseify(Utils.pluralize(typeName));
// add collectionName if missing
collection.collectionName = collectionName;
collection.options.collectionName = collectionName;
// add views
collection.views = [];
//register individual collection callback
registerCollectionCallback(typeName.toLowerCase());
// if schema has at least one intl field, add intl callback just before
// `${collectionName}.collection` callbacks run to make sure it always runs last
if (schemaHasIntlFields(schema)) {
hasIntlFields = true; // we have at least one intl field
addCallback(`${typeName.toLowerCase()}.collection`, addIntlFields);
}
//run schema callbacks and run general callbacks last
schema = runCallbacks({
name: `${typeName.toLowerCase()}.collection`,
iterator: schema,
properties: { options },
});
schema = runCallbacks({ name: '*.collection', iterator: schema, properties: { options } });
if (schema) {
// attach schema to collection
collection.attachSchema(createSchema(schema, apiSchema, dbSchema));
}
runCallbacksAsync({ name: '*.collection.async', properties: { options } });
runCallbacksAsync({ name: `${collectionName}.collection.async`, properties: { options } });
// ------------------------------------- Default Fragment -------------------------------- //
const defaultFragment = getDefaultFragmentText(collection);
if (defaultFragment) registerFragment(defaultFragment);
// ------------------------------------- Parameters -------------------------------- //
// legacy
collection.getParameters = (terms = {}, apolloClient, context) => {
// console.log(terms);
const currentSchema = collection.simpleSchema()._schema;
let parameters = {
selector: {},
options: {},
};
if (collection.defaultView) {
parameters = Utils.deepExtend(true, parameters, collection.defaultView(terms, apolloClient, context));
}
// handle view option
if (terms.view && collection.views[terms.view]) {
const viewFn = collection.views[terms.view];
const view = viewFn(terms, apolloClient, context);
let mergedParameters = Utils.deepExtend(true, parameters, view);
if (mergedParameters.options && mergedParameters.options.sort && view.options && view.options.sort) {
// If both the default view and the selected view have sort options,
// don't merge them together; take the selected view's sort. (Otherwise
// they merge in the wrong order, so that the default-view's sort takes
// precedence over the selected view's sort.)
mergedParameters.options.sort = view.options.sort;
}
parameters = mergedParameters;
}
// iterate over posts.parameters callbacks
parameters = runCallbacks(`${typeName.toLowerCase()}.parameters`, parameters, clone(terms), apolloClient, context);
// OpenCRUD backwards compatibility
parameters = runCallbacks(`${collectionName.toLowerCase()}.parameters`, parameters, clone(terms), apolloClient, context);
if (Meteor.isClient) {
parameters = runCallbacks(`${typeName.toLowerCase()}.parameters.client`, parameters, clone(terms), apolloClient);
// OpenCRUD backwards compatibility
parameters = runCallbacks(`${collectionName.toLowerCase()}.parameters.client`, parameters, clone(terms), apolloClient);
}
// note: check that context exists to avoid calling this from withList during SSR
if (Meteor.isServer && context) {
parameters = runCallbacks(`${typeName.toLowerCase()}.parameters.server`, parameters, clone(terms), context);
// OpenCRUD backwards compatibility
parameters = runCallbacks(`${collectionName.toLowerCase()}.parameters.server`, parameters, clone(terms), context);
}
// sort using terms.orderBy (overwrite defaultView's sort)
if (terms.orderBy && !isEmpty(terms.orderBy)) {
parameters.options.sort = terms.orderBy;
}
// if there is no sort, default to sorting by createdAt descending
if (!parameters.options.sort) {
parameters.options.sort = { createdAt: -1 };
}
// extend sort to sort posts by _id to break ties, unless there's already an id sort
// NOTE: always do this last to avoid overriding another sort
//if (!(parameters.options.sort && parameters.options.sort._id)) {
// parameters = Utils.deepExtend(true, parameters, { options: { sort: { _id: -1 } } });
//}
// remove any null fields (setting a field to null means it should be deleted)
Object.keys(parameters.selector).forEach(key => {
if (parameters.selector[key] === null) delete parameters.selector[key];
});
if (parameters.options.sort) {
Object.keys(parameters.options.sort).forEach(key => {
if (parameters.options.sort[key] === null) delete parameters.options.sort[key];
});
}
if (terms.query) {
const query = escapeStringRegexp(terms.query);
const searchableFieldNames = Object.keys(currentSchema).filter(fieldName => currentSchema[fieldName].searchable);
if (searchableFieldNames.length) {
parameters = Utils.deepExtend(true, parameters, {
selector: {
$or: searchableFieldNames.map(fieldName => ({
[fieldName]: { $regex: query, $options: 'i' },
})),
},
});
} else {
// eslint-disable-next-line no-console
console.warn(
`Warning: terms.query is set but schema ${
collection.options.typeName
} has no searchable field. Set "searchable: true" for at least one field to enable search.`
);
}
}
// limit number of items to 1000 by default
const maxDocuments = getSetting('maxDocumentsPerRequest', 1000);
const limit = terms.limit || parameters.options.limit;
parameters.options.limit = !limit || limit < 1 || limit > maxDocuments ? maxDocuments : limit;
// console.log(JSON.stringify(parameters, 2));
return parameters;
};
if (existingCollection) {
Collections[existingCollectionIndex] = existingCollection;
} else {
Collections.push(collection);
}
return collection;
};
//register collection creation hook for each collection
function registerCollectionCallback(typeName) {
registerCallback({
name: `${typeName}.collection`,
iterator: { schema: 'the schema of the collection' },
properties: [
{ schema: 'The schema of the collection' },
{ validationErrors: 'An Object that can be used to accumulate validation errors' },
],
runs: 'sync',
returns: 'schema',
description: 'Modifies schemas on collection creation',
});
}
//register colleciton creation hook
registerCallback({
name: '*.collection',
iterator: { schema: 'the schema of the collection' },
properties: [
{ schema: 'The schema of the collection' },
{ validationErrors: 'An object that can be used to accumulate validation errors' },
],
runs: 'sync',
returns: 'schema',
description: 'Modifies schemas on collection creation',
});
// generate foo_intl fields
export function addIntlFields(schema) {
Object.keys(schema).forEach(fieldName => {
const fieldSchema = schema[fieldName];
if (isIntlField(fieldSchema) && !schemaHasIntlField(schema, fieldName)) {
// remove `intl` to avoid treating new _intl field as a field to internationalize
// eslint-disable-next-line no-unused-vars
const { intl, ...propertiesToCopy } = schema[fieldName];
schema[`${fieldName}_intl`] = {
...propertiesToCopy, // copy properties from regular field
hidden: true,
type: Array,
isIntlData: true,
};
delete schema[`${fieldName}_intl`].intl;
schema[`${fieldName}_intl.$`] = {
type: getIntlString(),
};
// if original field is required, enable custom validation function instead of `optional` property
if (!schema[fieldName].optional) {
schema[`${fieldName}_intl`].optional = true;
schema[`${fieldName}_intl`].custom = validateIntlField;
}
// make original non-intl field optional
schema[fieldName].optional = true;
}
});
return schema;
}
================================================
FILE: packages/vulcan-lib/lib/modules/components.js
================================================
import { compose } from './compose';
import React from 'react';
import difference from 'lodash/difference';
export const Components = {}; // will be populated on startup
export const ComponentsTable = {}; // storage for infos about components
export const coreComponents = [
'Alert',
'Button',
'Modal',
'ModalTrigger',
'Table',
'FormComponentCheckbox',
'FormComponentCheckboxGroup',
'FormComponentDate',
'FormComponentDate2',
'FormComponentDateTime',
'FormComponentDefault',
'FormComponentText',
'FormComponentEmail',
'FormComponentNumber',
'FormComponentRadioGroup',
'FormComponentSelect',
'FormComponentSelectMultiple',
'FormComponentStaticText',
'FormComponentTextarea',
'FormComponentTime',
'FormComponentUrl',
'FormComponentInner',
'FormControl',
'FormElement',
];
/**
* Register a Vulcan component with a name, a raw component than can be extended
* and one or more optional higher order components.
*
* @param {String} name The name of the component to register.
* @param {Component} rawComponent Interchangeable/extendable react component.
* @param {...(Function|Array)} hocs The HOCs to compose with the raw component.
*
* Note: when a component is registered without higher order component, `hocs` will be
* an empty array, and it's ok!
* See https://github.com/reactjs/redux/blob/master/src/compose.js#L13-L15
*
* @returns Structure of a component in the list:
*
* ComponentsTable.Foo = {
* name: 'Foo',
* hocs: [fn1, fn2],
* rawComponent: React.Component,
* call: () => compose(...hocs)(rawComponent),
* }
*
*/
export function registerComponent(name, rawComponent, ...hocs) {
// support single-argument syntax
if (typeof arguments[0] === 'object') {
// note: cannot use `const` because name, components, hocs are already defined
// as arguments so destructuring cannot work
// eslint-disable-next-line no-redeclare
var { name, component, hocs = [] } = arguments[0];
rawComponent = component;
}
// store the component in the table
ComponentsTable[name] = {
name,
rawComponent,
hocs,
};
}
/**
* Returns true if a component with the given name has been registered with
* registerComponent(name, component, ...hocs).
*
* @param {String} name The name of the component to get.
* @returns {Boolean}
*/
export const componentExists = (name) => {
const component = ComponentsTable[name];
return !!component;
};
/**
* Get a component registered with registerComponent(name, component, ...hocs).
*
* @param {String} name The name of the component to get.
* @returns {Function|React Component} A (wrapped) React component
*/
export const getComponent = name => {
const component = ComponentsTable[name];
if (!component) {
throw new Error(`Component ${name} not registered.`);
}
if (component.hocs && component.hocs.length) {
const hocs = component.hocs.map(hoc => {
if (!Array.isArray(hoc)) {
if (typeof hoc !== 'function') {
throw new Error(`In registered component ${name}, an hoc is of type ${typeof hoc}`);
}
return hoc;
}
const [actualHoc, ...args] = hoc;
if (typeof actualHoc !== 'function') {
throw new Error(`In registered component ${name}, an hoc is of type ${typeof actualHoc}`);
}
return actualHoc(...args);
});
return compose(...hocs)(component.rawComponent);
} else {
return component.rawComponent;
}
};
/**
* Populate the lookup table for components to be callable
* ℹ️ Called once on app startup
**/
export const populateComponentsApp = () => {
const registeredComponents = Object.keys(ComponentsTable);
// loop over each component in the list
registeredComponents.map(name => {
// populate an entry in the lookup table
Components[name] = getComponent(name);
// uncomment for debug
// console.log('init component:', name);
});
const missingComponents = difference(coreComponents, registeredComponents);
if (missingComponents.length) {
// eslint-disable-next-line no-console
console.warn(
`Found the following missing core components: ${missingComponents.join(
', '
)}. Include a UI package such as vulcan:ui-bootstrap to add them.`
);
}
};
/**
* Get the **raw** (original) component registered with registerComponent
* without the possible HOCs wrapping it.
*
* @param {String} name The name of the component to get.
* @returns {Function|React Component} An interchangeable/extendable React component
*/
export const getRawComponent = name => {
return ComponentsTable[name].rawComponent;
};
/**
* Replace a Vulcan component with the same name with a new component or
* an extension of the raw component and one or more optional higher order components.
* This function keeps track of the previous HOCs and wrap the new HOCs around previous ones
*
* @param {String} name The name of the component to register.
* @param {React Component} newComponent Interchangeable/extendable component.
* @param {...Function} newHocs The HOCs to compose with the raw component.
* @returns {Function|React Component} A component callable with Components[name]
*
* Note: when a component is registered without higher order component, `hocs` will be
* an empty array, and it's ok!
* See https://github.com/reactjs/redux/blob/master/src/compose.js#L13-L15
*/
export function replaceComponent(name, newComponent, ...newHocs) {
// support single argument syntax
if (typeof arguments[0] === 'object') {
// eslint-disable-next-line no-redeclare
var { name, component, hocs = [] } = arguments[0];
newComponent = component;
newHocs = hocs;
}
const previousComponent = ComponentsTable[name];
const previousHocs = (previousComponent && previousComponent.hocs) || [];
if (!previousComponent) {
// eslint-disable-next-line no-console
console.warn(
`Trying to replace non-registered component ${name}. The component is ` +
'being registered. If you were trying to replace a component defined by ' +
"another package, make sure that you haven't misspelled the name. Check " +
'also if the original component is still being registered or that it ' +
"hasn't been renamed."
);
}
return registerComponent(name, newComponent, ...newHocs, ...previousHocs);
}
export const copyHoCs = (sourceComponent, targetComponent) => {
return compose(...sourceComponent.hocs)(targetComponent);
};
/**
* Returns an instance of the given component name of function
* @param {string|function} component A component, the name of a component, or a react element
* @param {Object} [props] Optional properties to pass to the component
*/
//eslint-disable-next-line react/display-name
export const instantiateComponent = (component, props) => {
if (!component) {
return null;
} else if (typeof component === 'string') {
const Component = Components[component];
return ;
} else if (React.isValidElement(component)) {
return React.cloneElement(component, props);
} else if (typeof component === 'function' &&
component.prototype &&
component.prototype.isReactComponent
) {
const Component = component;
return ;
} else if (typeof component === 'function') {
return component(props);
} else if (typeof component === 'object' && component.$$typeof && component.render) {
const Component = component;
return ;
} else {
return component;
}
};
/**
* Creates a component that will render the registered component with the given name.
*
* This function may be useful when in need for some registered component, but in contexts
* where they have not yet been initialized, for example at compile time execution. In other
* words, when using `Components.ComponentName` is not allowed (because it has not yet been
* populated, hence would be `undefined`), then `delayedComponent('ComponentName')` can be
* used instead.
*
* @example Create a container for a registered component
* // SomeContainer.js
* import { compose } from 'meteor/vulcan:lib';
* import { delayedComponent } from 'meteor/vulcan:core';
*
* export default compose(
* // ...some hocs with container logic
* )(delayedComponent('ComponentName')); // cannot use Components.ComponentName in this context!
*
* @example {@link dynamicLoader}
* @param {String} name Component name
* @return {Function}
* Functional component that will render the given registered component
*/
export const delayedComponent = name => {
return props => {
const Component = Components[name] || null;
return Component && ;
};
};
// Example with Proxy (might be unstable/hard to reason about)
//const mergeWithComponents = (myComponents = {}) => {
// const handler = {
// get: function(target, name) {
// return name in target ? target[name] : Components[name];
// }
// };
// const proxy = new Proxy(myComponents, handler);
// return proxy;
//};
export const mergeWithComponents = myComponents =>
myComponents ? { ...Components, ...myComponents } : Components;
================================================
FILE: packages/vulcan-lib/lib/modules/compose.js
================================================
import { createFactory } from 'react';
export const compose = (...funcs) => funcs.reduce((a, b) => (...args) => a(b(...args)), arg => arg);
export const setStatic = (key, value) => BaseComponent => {
/* eslint-disable no-param-reassign */
BaseComponent[key] = value;
/* eslint-enable no-param-reassign */
return BaseComponent;
};
export const getDisplayName = Component => {
if (typeof Component === 'string') {
return Component;
}
if (!Component) {
return undefined;
}
return Component.displayName || Component.name || 'Component';
};
export const wrapDisplayName = (BaseComponent, hocName) => `${hocName}(${getDisplayName(BaseComponent)})`;
export const setDisplayName = displayName => setStatic('displayName', displayName);
export const getContext = contextTypes => BaseComponent => {
const factory = createFactory(BaseComponent);
const GetContext = (ownerProps, context) =>
factory({
...ownerProps,
...context,
});
GetContext.contextTypes = contextTypes;
if (process.env.NODE_ENV !== 'production') {
return setDisplayName(wrapDisplayName(BaseComponent, 'getContext'))(GetContext);
}
return GetContext;
};
================================================
FILE: packages/vulcan-lib/lib/modules/config.js
================================================
import SimpleSchema from 'simpl-schema';
/**
* @summary Kick off the namespace for Vulcan.
* @namespace Vulcan
*/
// eslint-disable-next-line no-undef
Vulcan = {};
// eslint-disable-next-line no-undef
Vulcan.VERSION = '1.16.9';
// ------------------------------------- Schemas -------------------------------- //
export const additionalFieldKeys = [
'hidden', // hidden: true means the field is never shown in a form no matter what
'mustComplete', // mustComplete: true means the field is required to have a complete profile
'form', // extra form properties
'inputProperties', // extra form properties
'itemProperties', // extra properties for the form row
'input', // SmartForm control (String or React component)
'control', // SmartForm control (String or React component) (legacy)
'order', // position in the form
'group', // form fieldset group
'arrayItem', // properties for array items
'onCreate', // field insert callback
'onInsert', // field insert callback (OpenCRUD backwards compatibility)
'onUpdate', // field edit callback
'onEdit', // field edit callback (OpenCRUD backwards compatibility)
'onDelete', // field remove callback
'onRemove', // field remove callback (OpenCRUD backwards compatibility)
'canRead', // who can view the field
'viewableBy', // who can view the field (OpenCRUD backwards compatibility)
'canCreate', // who can insert the field
'insertableBy', // who can insert the field (OpenCRUD backwards compatibility)
'canUpdate', // who can edit the field
'editableBy', // who can edit the field (OpenCRUD backwards compatibility)
'typeName', // the type to resolve the field with
'resolveAs', // field-level resolver
'searchable', // whether a field is searchable
'description', // description/help
'beforeComponent', // before form component
'afterComponent', // after form component
'placeholder', // form field placeholder value
'options', // form options
'query', // field-specific data loading query
'dynamicQuery', // field-specific data loading query
'staticQuery', // field-specific data loading query
'queryWaitsForValue', // whether the data loading query should wait for a field to have a value to run
'autocompleteQuery', // query used to populate autocomplete
'selectable', // field can be used as part of a selector when querying for data
'unique', // field can be used as part of a selectorUnique when querying for data
'orderable', // field can be used to order results when querying for data (backwards-compatibility)
'sortable', // field can be used to order results when querying for data
'apiOnly', // field should not be inserted in database
'relation', // define a relation to another model
'intl', // set to `true` to make a field international
'isIntlData', // marker for the actual schema fields that hold intl strings
'intlId', // set an explicit i18n key for a field
];
SimpleSchema.extendOptions(additionalFieldKeys);
// eslint-disable-next-line no-undef
export default Vulcan;
================================================
FILE: packages/vulcan-lib/lib/modules/debug.js
================================================
import { getSetting } from './settings.js';
export const debug = function () {
if (getSetting('debug', false)) {
// eslint-disable-next-line no-console
console.log.apply(null, arguments);
}
};
export const debugGroup = function () {
if (getSetting('debug', false)) {
// eslint-disable-next-line no-console
console.groupCollapsed.apply(null, arguments);
}
};
export const debugGroupEnd = function () {
if (getSetting('debug', false)) {
// eslint-disable-next-line no-console
console.groupEnd.apply(null, arguments);
}
};
// Show a deprecation message, with a version so we keep track of deprecated features
export const deprecate = (nextVulcanVersion, message) => {
if (process.env.NODE_ENV === 'development') {
console.warn(`DEPRECATED (${nextVulcanVersion}):`, message);
}
};
================================================
FILE: packages/vulcan-lib/lib/modules/deep.js
================================================
/* eslint-disable */
// see https://gist.github.com/furf/3208381
_.mixin({
// Get/set the value of a nested property
deep: function (obj, key, value) {
var keys = key.replace(/\[(["']?)([^\1]+?)\1?\]/g, '.$2').replace(/^\./, '').split('.'),
root,
i = 0,
n = keys.length;
// Set deep value
if (arguments.length > 2) {
root = obj;
n--;
while (i < n) {
key = keys[i++];
obj = obj[key] = _.isObject(obj[key]) ? obj[key] : {};
}
obj[keys[i]] = value;
value = root;
// Get deep value
} else {
while ((obj = obj[keys[i++]]) !== null && i < n) {};
value = i < n ? void 0 : obj;
}
return value;
}
});
// Usage:
//
// var obj = {
// a: {
// b: {
// c: {
// d: ['e', 'f', 'g']
// }
// }
// }
// };
//
// Get deep value
// _.deep(obj, 'a.b.c.d[2]'); // 'g'
//
// Set deep value
// _.deep(obj, 'a.b.c.d[2]', 'george');
//
// _.deep(obj, 'a.b.c.d[2]'); // 'george'
_.mixin({
pluckDeep: function (obj, key) {
return _.map(obj, function (value) { return _.deep(value, key); });
}
});
_.mixin({
// Return a copy of an object containing all but the blacklisted properties.
unpick: function (obj) {
obj = obj || {};
return _.pick(obj, _.difference(_.keys(obj), _.flatten(Array.prototype.slice.call(arguments, 1))));
}
});
================================================
FILE: packages/vulcan-lib/lib/modules/deep_extend.js
================================================
import { Utils } from './utils.js';
// see: http://stackoverflow.com/questions/9399365/deep-extend-like-jquerys-for-nodejs
Utils.deepExtend = function () {
var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {},
i = 1,
length = arguments.length,
deep = false,
toString = Object.prototype.toString,
hasOwn = Object.prototype.hasOwnProperty,
class2type = {
'[object Boolean]': 'boolean',
'[object Number]': 'number',
'[object String]': 'string',
'[object Function]': 'function',
'[object Array]': 'array',
'[object Date]': 'date',
'[object RegExp]': 'regexp',
'[object Object]': 'object'
},
jQuery = {
isFunction: function (obj) {
return jQuery.type(obj) === 'function';
},
isArray: Array.isArray ||
function (obj) {
return jQuery.type(obj) === 'array';
},
isWindow: function (obj) {
return obj !== null && obj === obj.window;
},
isNumeric: function (obj) {
return !isNaN(parseFloat(obj)) && isFinite(obj);
},
type: function (obj) {
return obj === null ? String(obj) : class2type[toString.call(obj)] || 'object';
},
isPlainObject: function (obj) {
if (!obj || jQuery.type(obj) !== 'object' || obj.nodeType) {
return false;
}
try {
if (obj.constructor && !hasOwn.call(obj, 'constructor') && !hasOwn.call(obj.constructor.prototype, 'isPrototypeOf')) {
return false;
}
} catch (e) {
return false;
}
var key;
return key === undefined || hasOwn.call(obj, key);
}
};
if (typeof target === 'boolean') {
deep = target;
target = arguments[1] || {};
i = 2;
}
if (typeof target !== 'object' && !jQuery.isFunction(target)) {
target = {};
}
if (length === i) {
target = this;
--i;
}
for (i; i < length; i++) {
if ((options = arguments[i]) !== null) {
for (name in options) {
src = target[name];
copy = options[name];
if (target === copy) {
continue;
}
if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
if (copyIsArray) {
copyIsArray = false;
clone = src && jQuery.isArray(src) ? src : [];
} else {
clone = src && jQuery.isPlainObject(src) ? src : {};
}
// WARNING: RECURSION
target[name] = Utils.deepExtend(deep, clone, copy);
} else if (copy !== undefined) {
target[name] = copy;
}
}
}
}
return target;
};
================================================
FILE: packages/vulcan-lib/lib/modules/dynamic_loader.js
================================================
import React from 'react';
import loadable from 'react-loadable';
import isFunction from 'lodash/isFunction';
import { delayedComponent } from './components';
/**
* @callback dynamicLoader~importComponent
* @return {Promise}
*/
/**
* Returns a component that will perform the given dynamic import and render
* `Components.DynamicLoading` in the meantime.
*
* @example Register a component with a dynamic import
* registerComponent('MyComponent', dynamicLoader(() => import('./path/to/MyComponent')));
*
* @example Pass a dynamic component to a route
* import { addRoute, dynamicLoader, getDynamicComponent } from 'meteor/vulcan:core';
*
* addRoute({
* name: 'home',
* path: '/',
* component: dynamicLoader(() => import('./path/to/HomeComponent')),
* });
*
* @param {dynamicLoader~importComponent|Promise} importComponent
* Function where the dynamic import is performed
* @return {React.Component}
* Component that will load the dynamic import on mount
*/
export const dynamicLoader = importComponent =>
loadable({
loader: isFunction(importComponent) ? importComponent : () => importComponent, // backwards compatibility,
// use delayedComponent, as this function can be used when Components is not populated yet
loading: delayedComponent('DynamicLoading'),
});
/**
* Renders a dynamic component with the given props.
*
* @param {dynamicLoader~importComponent|Promise} importComponent
* @param {Object} props
*/
export const renderDynamicComponent = (importComponent, props = {}) =>
React.createElement(dynamicLoader(importComponent), props);
export const getDynamicComponent = componentImport => {
// eslint-disable-next-line no-console
console.warn(
'getDynamicComponent is deprecated, use renderDynamicComponent instead.',
'If you want to retrieve the component instead that of just rendering it,',
'use dynamicLoader. See this issue to know how to do it: https://github.com/VulcanJS/Vulcan/issues/1997'
);
return renderDynamicComponent(componentImport);
};
================================================
FILE: packages/vulcan-lib/lib/modules/errors.js
================================================
import get from 'lodash/get';
/*
Get whatever word is contained between the first two double quotes
*/
const getFirstWord = input => {
const parts = /"([^"]*)"/.exec(input);
if (parts === null) {
return null;
}
return parts[1];
};
/*
Parse a GraphQL error message
TODO: check if still useful?
Sample message:
"GraphQL error: Variable "$data" got invalid value {"meetingDate":"2018-08-07T06:05:51.704Z"}.
In field "name": Expected "String!", found null.
In field "stage": Expected "String!", found null.
In field "addresses": Expected "[JSON]!", found null."
*/
export const parseErrorMessage = message => {
if (!message) {
return null;
}
// note: optionally add .slice(1) at the end to get rid of the first error, which is not that helpful
let fieldErrors = message.split('\n');
fieldErrors = fieldErrors.map(error => {
// field name is whatever is between the first to double quotes
const fieldName = getFirstWord(error);
if (error.includes('found null')) {
// missing field errors
return {
id: 'errors.required',
path: fieldName,
properties: {
name: fieldName,
},
};
} else {
// other generic GraphQL errors
return {
message: error,
};
}
});
return fieldErrors;
};
/*
Errors can have the following properties stored on their `data` property:
- id: used as an internationalization key, for example `errors.required`
- path: for field-specific errors inside forms, the path of the field with the issue
- properties: additional data. Will be passed to vulcan-i18n as values
- message: if id cannot be used as i81n key, message will be used
*/
export const getErrors = error => {
const graphQLErrors = error.graphQLErrors;
// error thrown using new ApolloError
const apolloErrors = get(graphQLErrors, '0.extensions.data.errors');
// regular server error (with schema stitching)
const regularErrors = get(graphQLErrors, '0.extensions.exception.errors');
return apolloErrors || regularErrors || graphQLErrors || [];
};
================================================
FILE: packages/vulcan-lib/lib/modules/findbyids.js
================================================
import { Connectors } from '../server/connectors.js';
/**
* @summary Find by ids, for DataLoader, inspired by https://github.com/tmeasday/mongo-find-by-ids/blob/master/index.js
*/
const findByIds = async function(collection, ids, context) {
// get documents
const documents = await Connectors.find(collection, { _id: { $in: ids } });
// order documents in the same order as the ids passed as argument
const orderedDocuments = ids.map(id => _.findWhere(documents, {_id: id}));
return orderedDocuments;
};
export default findByIds;
================================================
FILE: packages/vulcan-lib/lib/modules/fragment_matcher.js
================================================
import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
export const FragmentMatcher = [];
export const addToFragmentMatcher = fragmentMatcher => {
FragmentMatcher.push(fragmentMatcher);
};
export const getFragmentMatcher = () => {
const fm = {
introspectionQueryResultData: {
__schema: {
types: FragmentMatcher,
},
}
};
return new IntrospectionFragmentMatcher(fm);
};
================================================
FILE: packages/vulcan-lib/lib/modules/fragments.js
================================================
import gql from 'graphql-tag';
import { getDefaultFragmentText } from './graphql/defaultFragment';
import uniq from 'lodash/uniq';
import flattenDeep from 'lodash/flattenDeep';
import stringSimilarity from 'string-similarity';
export const Fragments = {};
export const FragmentsExtensions = {}; // will be used on startup
export const throwUnregisteredFragmentError = fragmentName => {
const similarFragments = stringSimilarity.findBestMatch(fragmentName, Object.keys(Fragments));
throw new Error(`A registered fragment named "${fragmentName}" cannot be found, did you mean "${similarFragments.bestMatch.target}"?`);
};
/**
* @param {*} collectionOrName A collection name, or a whole collection
*/
export const getDefaultFragmentName = (collectionOrName) => {
const collectionName = typeof collectionOrName === 'string' ? collectionOrName : collectionOrName.options.collectionName;
return `${collectionName}DefaultFragment`;
};
/*
Get a fragment's name from its text
*/
export const extractFragmentName = fragmentText => fragmentText.match(/fragment (.*) on/)[1];
/*
Get a query resolver's name from its text
*/
export const extractResolverName = resolverText => resolverText.trim().substr(0, resolverText.trim().indexOf('{'));
/*
Register a fragment, including its text, the text of its subfragments, and the fragment object
*/
export const registerFragment = fragmentTextSource => {
// remove comments
const fragmentText = fragmentTextSource.replace(/\#.*\n/g, '\n');
// extract name from fragment text
const fragmentName = extractFragmentName(fragmentText);
// extract subFragments from text
const matchedSubFragments = fragmentText.match(/\.{3}([_A-Za-z][_0-9A-Za-z]*)/g) || [];
const subFragments = _.unique(matchedSubFragments.map(f => f.replace('...', '')));
// register fragment
Fragments[fragmentName] = {
fragmentText
};
// also add subfragments if there are any
if (subFragments && subFragments.length) {
Fragments[fragmentName].subFragments = subFragments;
}
};
/*
Create gql fragment object from text and subfragments
*/
export const getFragmentObject = (fragmentText, subFragments) => {
// pad the literals array with line returns for each subFragments
const literals = subFragments ? [fragmentText, ...subFragments.map(x => '\n')] : [fragmentText];
// the gql function expects an array of literals as first argument, and then sub-fragments as other arguments
const gqlArguments = subFragments ? [literals, ...subFragments.map(subFragmentName => {
// return subfragment's gql fragment
if (!Fragments[subFragmentName] || !Fragments[subFragmentName].fragmentObject) {
throw new Error(`Subfragment “${subFragmentName}” of fragment “${extractFragmentName(fragmentText)}” has not been initialized yet.`);
}
return Fragments[subFragmentName].fragmentObject;
})] : [literals];
return gql.apply(null, gqlArguments);
};
export const getDefaultFragment = collection => {
const fragmentText = getDefaultFragmentText(collection);
return fragmentText ? gql`${fragmentText}` : null;
};
/*
Queue a fragment to be extended with additional properties.
Note: can be used even before the fragment has been registered.
*/
export const extendFragment = (fragmentName, newProperties) => {
FragmentsExtensions[fragmentName] = FragmentsExtensions[fragmentName] ? [...FragmentsExtensions[fragmentName], newProperties] : [newProperties];
};
/*
Perform fragment extension (called from initializeFragments()
Note: will call registerFragment again each time, resulting in multiple fragments
with the same name (but duplicate fragments warning is disabled).
*/
export const extendFragmentWithProperties = (fragmentName, newProperties) => {
const fragment = Fragments[fragmentName];
const fragmentEndPosition = fragment.fragmentText.lastIndexOf('}');
const newFragmentText = [
fragment.fragmentText.slice(0, fragmentEndPosition),
newProperties,
fragment.fragmentText.slice(fragmentEndPosition)
].join('');
registerFragment(newFragmentText);
};
/*
Remove a property from a fragment
Note: can only be called *after* a fragment is registered
*/
export const removeFromFragment = (fragmentName, propertyName) => {
const fragment = Fragments[fragmentName];
const newFragmentText = fragment.fragmentText.replace(propertyName, '');
registerFragment(newFragmentText);
};
/*
Get fragment name from fragment object
*/
export const getFragmentName = fragment => fragment && fragment.definitions[0] && fragment.definitions[0].name.value;
/*
Get actual gql fragment
*/
export const getFragment = fragmentName => {
if (!Fragments[fragmentName]) {
throwUnregisteredFragmentError(fragmentName);
}
if (!Fragments[fragmentName].fragmentObject) {
initializeFragments([fragmentName]);
}
// return fragment object created by gql
return Fragments[fragmentName].fragmentObject;
};
/*
Get gql fragment text
*/
export const getFragmentText = fragmentName => {
if (!Fragments[fragmentName]) {
throwUnregisteredFragmentError(fragmentName);
}
// return fragment object created by gql
return Fragments[fragmentName].fragmentText;
};
/*
Get names of non initialized fragments.
*/
export const getNonInitializedFragmentNames = () =>
_.keys(Fragments).filter(name => !Fragments[name].fragmentObject);
/*
Perform all fragment extensions (called from routing)
*/
export const initializeFragments = (fragments = getNonInitializedFragmentNames()) => {
const errorFragmentKeys = [];
// extend fragment texts (if extended fragment exists)
_.forEach(FragmentsExtensions, (extensions, fragmentName) => {
if (Fragments[fragmentName]) {
extensions.forEach(newProperties => {
extendFragmentWithProperties(fragmentName, newProperties);
});
}
});
// create fragment objects
// initialize fragments *with no subfragments* first to avoid unresolved dependencies
const keysWithoutSubFragments = _.filter(fragments, fragmentName => !Fragments[fragmentName].subFragments);
_.forEach(keysWithoutSubFragments, fragmentName => {
const fragment = Fragments[fragmentName];
fragment.fragmentObject = getFragmentObject(fragment.fragmentText, fragment.subFragments);
});
// next, initialize fragments that *have* subfragments
const keysWithSubFragments = _.filter(_.keys(Fragments), fragmentName => !!Fragments[fragmentName].subFragments);
_.forEach(keysWithSubFragments, fragmentName => {
const fragment = Fragments[fragmentName];
try {
fragment.fragmentObject = getFragmentObject(fragment.fragmentText, fragment.subFragments);
} catch (error) {
// if fragment initialization triggers an error, store fragment and try again later
// common error causes include cross-dependencies
errorFragmentKeys.push(fragmentName);
}
});
// finally, try initializing any fragment that triggered an error again
_.forEach(errorFragmentKeys, fragmentName => {
const fragment = Fragments[fragmentName];
fragment.fragmentObject = getFragmentObject(fragment.fragmentText, fragment.subFragments);
});
};
/*
Take a text query, and expand any subfragments inside it
*/
export const expandQueryFragments = query => {
let expandedQuery = query;
// get all fragment names
const fragmentNames = extractSubFragmentsFlat(query);
// append each fragment text to the end of the query
fragmentNames.forEach(fragmentName => {
expandedQuery = expandedQuery + '\n' + Fragments[fragmentName].fragmentText;
});
return expandedQuery;
};
/*
Recursively extract all nested fragment dependency names into nested arrays
Works on any string (query or fragment)
Note: only extracts *sub*fragments (e.g. not the current fragment itself)
*/
export const extractSubFragments = (text) => {
// extract subFragments from text
const matchedSubFragments = text.match(/\.{3}([_A-Za-z][_0-9A-Za-z]*)/g) || [];
if (matchedSubFragments.length > 0) {
// return an array of arrays
return matchedSubFragments.map(s => {
const subFragmentName = s.replace('...', '');
if (!Fragments[subFragmentName]) {
throwUnregisteredFragmentError(subFragmentName);
}
const subFragmentText = Fragments[subFragmentName].fragmentText;
// Return the name of the matched subfragment, then call function recursively
return [subFragmentName, ...extractSubFragments(subFragmentText)];
});
} else {
return [];
}
};
/*
Flatten nested fragments array and only keep unique fragment names
*/
export const extractSubFragmentsFlat = text => uniq(flattenDeep(extractSubFragments(text)));
================================================
FILE: packages/vulcan-lib/lib/modules/graphql/defaultFragment.js
================================================
/**
* Generates the default fragment for a collection
* = a fragment containing all fields
*/
import { getFragmentFieldNames } from '../schema_utils';
import { isBlackbox } from '../simpleSchema_utils';
const intlSuffix = '_intl';
// get fragment for a whole object (root schema or nested schema of an object or an array)
const getObjectFragment = ({
schema,
fragmentName,
options
}) => {
const fieldNames = getFragmentFieldNames({ schema, options });
const childFragments = fieldNames.length && fieldNames.map(fieldName => getFieldFragment({
schema,
fieldName,
options,
getObjectFragment: getObjectFragment
}))
// remove empty values
.filter(f => !!f);
if (childFragments.length) {
return `${fragmentName} { ${childFragments.join('\n')} }`;
}
return null;
};
// get fragment for a specific field (either the field name or a nested fragment)
export const getFieldFragment = ({
schema,
fieldName,
options,
getObjectFragment = getObjectFragment // a callback to call on nested schema
}) => {
// intl
if (fieldName.slice(-5) === intlSuffix) {
return `${fieldName}{ locale value }`;
}
if (fieldName === '_id') return fieldName;
const field = schema[fieldName];
const fieldType = field.type.singleType;
const fieldTypeName =
typeof fieldType === 'object' ? 'Object' : typeof fieldType === 'function' ? fieldType.name : fieldType;
switch (fieldTypeName) {
case 'Object':
if (!isBlackbox(field) && fieldType._schema) {
return getObjectFragment({
fragmentName: fieldName,
schema: fieldType._schema,
options
}) || null;
}
return fieldName;
case 'Array':
const arrayItemFieldName = `${fieldName}.$`;
const arrayItemField = schema[arrayItemFieldName];
// note: make sure field has an associated array item field
if (arrayItemField) {
// child will either be native value or a an object (first case)
const arrayItemFieldType = arrayItemField.type.singleType;
if (!isBlackbox(field) && arrayItemFieldType._schema) {
return getObjectFragment({
fragmentName: fieldName,
schema: arrayItemFieldType._schema,
options
}) || null;
}
}
return fieldName;
default:
return fieldName; // fragment = fieldName
}
};
/*
Create default "dumb" gql fragment object for a given collection
*/
export const getDefaultFragmentText = (collection, options = { onlyViewable: true }) => {
const schema = collection.simpleSchema()._schema;
return getObjectFragment({
schema,
fragmentName: `fragment ${collection.options.collectionName}DefaultFragment on ${collection.typeName}`,
options
}) || null;
};
export default getDefaultFragmentText;
================================================
FILE: packages/vulcan-lib/lib/modules/graphql/index.js
================================================
export * from './defaultFragment';
export * from './utils';
================================================
FILE: packages/vulcan-lib/lib/modules/graphql/utils.js
================================================
import { Utils } from '../utils';
import { isBlackbox, unarrayfyFieldName, getFieldType, getFieldTypeName } from '../simpleSchema_utils';
export const getGraphQLType = ({ fieldSchema, schema, fieldName, typeName, isInput = false, isParentBlackbox = false }) => {
const field = fieldSchema || schema[fieldName];
if (field.typeName) return field.typeName; // respect typeName provided by user
const fieldType = getFieldType(field);
const fieldTypeName = getFieldTypeName(fieldType);
// NOTE: we DON't USE isInputField! we don't want to match "field.intl", only "field.intlData"
/**
* Expected GraphQL Schema:
*
* # The room name
* name(locale: String): String @intl
* # The room name
* name_intl(locale: String): [IntlValue] @intl
*
* JS schema:
*
* name: {
* type: String,
* optional: false,
* canRead: ['guests'],
* canCreate: ['admins'],
* intl: true,
* },
*/
if (field.isIntlData) {
return isInput ? '[IntlValueInput]' : '[IntlValue]';
}
switch (fieldTypeName) {
case 'String':
/*
Getting Enums from allowed values is counter productive because enums syntax is limited
@see https://github.com/VulcanJS/Vulcan/issues/2332
if (hasAllowedValues(field) && isValidEnum(getAllowedValues(field))) {
return getEnumType(typeName, fieldName);
}*/
return 'String';
case 'Boolean':
return 'Boolean';
case 'Number':
return 'Float';
case 'SimpleSchema.Integer':
return 'Int';
// for arrays, look for type of associated schema field or default to [String]
case 'Array':
const arrayItemFieldName = `${fieldName}.$`;
// note: make sure field has an associated array
if (schema[arrayItemFieldName]) {
// try to get array type from associated array
const arrayItemType = getGraphQLType({
schema,
fieldName: arrayItemFieldName,
typeName,
isInput,
isParentBlackbox: isParentBlackbox || isBlackbox(field) // blackbox field may not be nested items
});
return arrayItemType ? `[${arrayItemType}]` : null;
}
return null;
case 'Object':
// 4 cases:
// - it's the child of a blackboxed array => will be blackbox JSON
// - a nested Schema,
// - a referenced schema, or an actual JSON
if (isParentBlackbox) return 'JSON';
if (!isBlackbox(field) && fieldType._schema) {
return getNestedGraphQLType(typeName, fieldName, isInput);
}
// referenced Schema
if (/*field.type.definitions[0].blackbox && */field.typeName && field.typeName !== 'JSON') {
return isInput ? field.typeName + 'Input' : field.typeName;
}
// blackbox JSON object
return 'JSON';
case 'Date':
return 'Date';
default:
return null;
}
};
// get GraphQL type for a nested object ( e.g PostAuthor, EventAdress, etc.)
export const getNestedGraphQLType = (typeName, fieldName, isInput) =>
`${typeName}${Utils.capitalize(unarrayfyFieldName(fieldName))}${isInput ? 'Input' : ''}`;
================================================
FILE: packages/vulcan-lib/lib/modules/graphql_templates/filtering.js
================================================
import { convertToGraphQL } from './types.js';
import { Utils } from '../utils.js';
// field types that support filtering
const supportedFieldTypes = ['String', 'Int', 'Float', 'Boolean', 'Date'];
const getContentType = type =>
type
.replace('[', '')
.replace(']', '')
.replace('!', '');
const isSupportedFieldType = type => supportedFieldTypes.includes(type);
/* ------------------------------------- Selector Types ------------------------------------- */
/*
The selector type is used to query for one or more documents
type MovieSelectorInput {
AND: [MovieSelectorInput]
OR: [MovieSelectorInput]
...
}
// TODO: not currently used
*/
export const selectorInputType = typeName => `${typeName}SelectorInput`;
export const selectorInputTemplate = ({ typeName, fields }) =>
`input ${selectorInputType(typeName)} {
_and: [${selectorInputType(typeName)}]
_or: [${selectorInputType(typeName)}]
${convertToGraphQL(fields, ' ')}
}`;
/*
The unique selector type is used to query for exactly one document
type MovieSelectorUniqueInput {
_id: String
slug: String
}
*/
export const selectorUniqueInputType = typeName => `${typeName}SelectorUniqueInput`;
export const selectorUniqueInputTemplate = ({ typeName, fields }) =>
`input ${selectorUniqueInputType(typeName)} {
_id: String
documentId: String # OpenCRUD backwards compatibility
slug: String
${convertToGraphQL(fields, ' ')}
}`;
const formatFilterName = s => Utils.capitalize(s.replace('_', ''));
/*
See https://docs.hasura.io/1.0/graphql/manual/queries/query-filters.html#
Note: if a filter doesn't take arguments just use a boolean (e.g. `_onlyPublic: true`)
instead of defining a custom type.
*/
export const filterInputType = typeName => `${typeName}FilterInput`;
export const fieldFilterInputTemplate = ({ typeName, fields, customFilters = [], customSorts = [] }) =>
`input ${filterInputType(typeName)} {
_and: [${filterInputType(typeName)}]
_not: ${filterInputType(typeName)}
_or: [${filterInputType(typeName)}]
${customFilters.map(filter => ` ${filter.name}: ${filter.arguments ? customFilterType(typeName, filter) : 'Boolean'}`)}
${customSorts.map(sort => ` ${sort.name}: ${customSortType(typeName, sort)}`)}
${fields
.map(field => {
const { name, type } = field;
const contentType = getContentType(type);
if (isSupportedFieldType(contentType)) {
const isArrayField = type[0] === '[';
return ` ${name}: ${contentType}_${isArrayField ? 'Array_' : ''}Selector`;
} else {
return '';
}
})
.join('\n')}
}`;
export const sortInputType = typeName => `${typeName}SortInput`;
export const fieldSortInputTemplate = ({ typeName, fields }) =>
`input ${sortInputType(typeName)} {
${fields.map(({ name }) => ` ${name}: SortOptions`).join('\n')}
}`;
export const customFilterType = (typeName, filter) => `${typeName}${formatFilterName(filter.name)}FilterInput`;
export const customFilterTemplate = ({ typeName, filter }) =>
`input ${customFilterType(typeName, filter)}{
${filter.arguments}
}`;
// TODO: not currently used
export const customSortType = (typeName, filter) => `${typeName}${formatFilterName(filter.name)}SortInput`;
export const customSortTemplate = ({ typeName, sort }) =>
`input ${customSortType(typeName, sort)}{
${sort.arguments}
}`;
// export const customFilterTemplate = ({ typeName, customFilters }) =>
// `enum ${typeName}CustomFilter{
// ${Object.keys(customFilters).map(name => ` ${name}`).join('\n')}
// }`;
// export const customSortTemplate = ({ typeName, customFilters }) =>
// `enum ${typeName}CustomSort{
// ${Object.keys(customFilters).map(name => ` ${name}`).join('\n')}
// }`;
/*
export const orderByInputTemplate = ({ typeName, fields }) =>
`enum ${typeName}SortInput {
${Array.isArray(fields) && fields.length ? fields.join('\n ') : 'foobar'}
}`;
*/
================================================
FILE: packages/vulcan-lib/lib/modules/graphql_templates/index.js
================================================
export * from './types.js';
export * from './queries.js';
export * from './mutations.js';
export * from './filtering.js';
export * from './other.js';
================================================
FILE: packages/vulcan-lib/lib/modules/graphql_templates/mutations.js
================================================
import { convertToGraphQL } from './types.js';
import { filterInputType, selectorUniqueInputType } from './filtering.js';
// eslint-disable-next-line
const deprecated = `# Deprecated (use 'input' field instead).`;
const mutationReturnProperty = 'data';
/* ------------------------------------- Mutation Types ------------------------------------- */
/*
Mutation for creating a new document
createMovie(input: CreateMovieInput) : MovieOutput
*/
export const createMutationType = typeName => `create${typeName}`;
export const createMutationTemplate = ({ typeName }) =>
`${createMutationType(typeName)}(
input: ${createInputType(typeName, false)},
${deprecated}
data: ${createDataInputType(typeName, false)}
) : ${mutationOutputType(typeName)}`;
/*
Mutation for updating an existing document
updateMovie(input: UpdateMovieInput) : MovieOutput
*/
export const updateMutationType = typeName => `update${typeName}`;
export const updateMutationTemplate = ({ typeName }) =>
`${updateMutationType(typeName)}(
input: ${updateInputType(typeName, false)},
${deprecated}
selector: ${selectorUniqueInputType(typeName)},
${deprecated}
data: ${updateDataInputType(typeName)}
) : ${mutationOutputType(typeName)}`;
/*
Mutation for updating an existing document; or creating it if it doesn't exist yet
upsertMovie(input: UpsertMovieInput) : MovieOutput
*/
export const upsertMutationType = typeName => `upsert${typeName}`;
export const upsertMutationTemplate = ({ typeName }) =>
`${upsertMutationType(typeName)}(
input: ${upsertInputType(typeName, false)},
${deprecated}
selector: ${selectorUniqueInputType(typeName)},
${deprecated}
data: ${updateDataInputType(typeName, false)}
) : ${mutationOutputType(typeName)}`;
/*
Mutation for deleting an existing document
deleteMovie(input: DeleteMovieInput) : MovieOutput
*/
export const deleteMutationType = typeName => `delete${typeName}`;
export const deleteMutationTemplate = ({ typeName }) =>
`${deleteMutationType(typeName)}(
input: ${deleteInputType(typeName, false)},
${deprecated}
selector: ${selectorUniqueInputType(typeName)}
) : ${mutationOutputType(typeName)}`;
/* ------------------------------------- Mutation Input Types ------------------------------------- */
/*
Type for create mutation input argument
type CreateMovieInput {
data: CreateMovieDataInput!
}
*/
export const createInputType = typeName => `Create${typeName}Input`;
export const createInputTemplate = ({ typeName }) =>
`input ${createInputType(typeName)} {
data: ${createDataInputType(typeName, true)}
# An identifier to name the mutation's execution context
contextName: String
}`;
/*
Type for update mutation input argument
type UpdateMovieInput {
selector: MovieSelectorUniqueInput!
data: UpdateMovieDataInput!
}
Note: selector is for backwards-compatibility
*/
export const updateInputType = typeName => `Update${typeName}Input`;
export const updateInputTemplate = ({ typeName }) =>
`input ${updateInputType(typeName)}{
filter: ${filterInputType(typeName)}
id: String
data: ${updateDataInputType(typeName, true)}
# An identifier to name the mutation's execution context
contextName: String
}`;
/*
Type for upsert mutation input argument
Note: upsertInputTemplate uses same data type as updateInputTemplate
type UpsertMovieInput {
selector: MovieSelectorUniqueInput!
data: UpdateMovieDataInput!
}
Note: selector is for backwards-compatibility
*/
export const upsertInputType = typeName => `Upsert${typeName}Input`;
export const upsertInputTemplate = ({ typeName }) =>
`input ${upsertInputType(typeName)}{
filter: ${filterInputType(typeName)}
id: String
data: ${updateDataInputType(typeName, true)}
# An identifier to name the mutation's execution context
contextName: String
}`;
/*
Type for delete mutation input argument
type DeleteMovieInput {
selector: MovieSelectorUniqueInput!
}
Note: selector is for backwards-compatibility
*/
export const deleteInputType = typeName => `Delete${typeName}Input`;
export const deleteInputTemplate = ({ typeName }) =>
`input ${deleteInputType(typeName)}{
filter: ${filterInputType(typeName)}
id: String
}`;
/*
Type for the create mutation input argument's data property
type CreateMovieDataInput {
title: String
description: String
}
*/
export const createDataInputType = (typeName, nonNull = false) => `Create${typeName}DataInput${nonNull ? '!' : ''}`;
export const createDataInputTemplate = ({ typeName, fields }) =>
`input ${createDataInputType(typeName)} {
${convertToGraphQL(fields, ' ')}
}`;
/*
Type for the update & upsert mutations input argument's data property
type UpdateMovieDataInput {
title: String
description: String
}
*/
export const updateDataInputType = (typeName, nonNull = false) => `Update${typeName}DataInput${nonNull ? '!' : ''}`;
export const updateDataInputTemplate = ({ typeName, fields }) =>
`input ${updateDataInputType(typeName)} {
${convertToGraphQL(fields, ' ')}
}`;
/* ------------------------------------- Mutation Output Type ------------------------------------- */
/*
Type for the return value of all mutations
type MovieOutput {
data: Movie
}
*/
export const mutationOutputType = typeName => `${typeName}MutationOutput`;
export const mutationOutputTemplate = ({ typeName }) =>
`type ${mutationOutputType(typeName)}{
${mutationReturnProperty}: ${typeName}
}`;
/* ------------------------------------- Mutation Queries ------------------------------------- */
/*
Create mutation query used on the client
mutation createMovie($data: CreateMovieDataInput!) {
createMovie(data: $data) {
data {
_id
name
__typename
}
__typename
}
}
*/
export const createClientTemplate = ({ typeName, fragmentName }) =>
`mutation ${createMutationType(typeName)}($input: ${createInputType(typeName)}, $data: ${createDataInputType(typeName)}) {
${createMutationType(typeName)}(input: $input, data: $data) {
${mutationReturnProperty} {
...${fragmentName}
}
}
}`;
/*
Update mutation query used on the client
mutation updateMovie($selector: MovieSelectorUniqueInput!, $data: UpdateMovieDataInput!) {
updateMovie(selector: $selector, data: $data) {
data {
_id
name
__typename
}
__typename
}
}
*/
export const updateClientTemplate = ({ typeName, fragmentName }) =>
`mutation ${updateMutationType(typeName)}($input: ${updateInputType(typeName)}, $selector: ${selectorUniqueInputType(
typeName
)}, $data: ${updateDataInputType(typeName, false)}) {
${updateMutationType(typeName)}(input: $input, selector: $selector, data: $data) {
${mutationReturnProperty} {
...${fragmentName}
}
}
}`;
/*
Upsert mutation query used on the client
mutation upsertMovie($selector: MovieSelectorUniqueInput!, $data: UpdateMovieDataInput!) {
upsertMovie(selector: $selector, data: $data) {
data {
_id
name
__typename
}
__typename
}
}
*/
export const upsertClientTemplate = ({ typeName, fragmentName }) =>
`mutation ${upsertMutationType(typeName)}($input: ${upsertInputType(typeName)}, $selector: ${selectorUniqueInputType(
typeName
)}, $data: ${updateDataInputType(typeName, false)}) {
${upsertMutationType(typeName)}(input: $input, selector: $selector, data: $data) {
${mutationReturnProperty} {
...${fragmentName}
}
}
}`;
/*
Delete mutation query used on the client
mutation deleteMovie($selector: MovieSelectorUniqueInput!) {
deleteMovie(selector: $selector) {
data {
_id
name
__typename
}
__typename
}
}
*/
export const deleteClientTemplate = ({ typeName, fragmentName }) =>
`mutation ${deleteMutationType(typeName)}($input: ${deleteInputType(typeName)}, $selector: ${selectorUniqueInputType(typeName)}) {
${deleteMutationType(typeName)}(input: $input, selector: $selector) {
${mutationReturnProperty} {
...${fragmentName}
}
}
}`;
================================================
FILE: packages/vulcan-lib/lib/modules/graphql_templates/other.js
================================================
import { capitalize } from '../utils';
/*
Field-specific data loading query template for a dynamic array of item IDs
(example: `categoriesIds` where $value is ['foo123', 'bar456'])
*/
export const fieldDynamicQueryTemplate = ({ queryResolverName, autocompletePropertyName, valuePropertyName = '_id', fragmentName }) =>
`query FormComponentDynamic${capitalize(queryResolverName)}Query($value: [String!]) {
${queryResolverName}(input: {
filter: { ${valuePropertyName}: { _in: $value } },
sort: { ${autocompletePropertyName}: asc }
}){
results{
${valuePropertyName}
${autocompletePropertyName}
${fragmentName && `...${fragmentName}` || ''}
}
}
}
`;
/*
Field-specific data loading query template for *all* items in a collection
*/
export const fieldStaticQueryTemplate = ({ queryResolverName, autocompletePropertyName, valuePropertyName = '_id', fragmentName }) =>
`query FormComponentStatic${capitalize(queryResolverName)}Query {
${queryResolverName}(input: {
sort: { ${autocompletePropertyName}: asc }
}){
results{
${valuePropertyName}
${autocompletePropertyName}
${fragmentName && `...${fragmentName}` || ''}
}
}
}
`;
/*
Query template for loading a list of autocomplete suggestions
*/
export const autocompleteQueryTemplate = ({ queryResolverName, autocompletePropertyName, valuePropertyName = '_id', fragmentName }) => `
query Autocomplete${capitalize(queryResolverName)}Query($queryString: String) {
${queryResolverName}(
input: {
filter: {
${autocompletePropertyName}: { _like: $queryString }
},
limit: 20
}
){
results{
${valuePropertyName}
${autocompletePropertyName}
${fragmentName && `...${fragmentName}` || ''}
}
}
}
`;
================================================
FILE: packages/vulcan-lib/lib/modules/graphql_templates/queries.js
================================================
import { Utils } from '../utils.js';
import { selectorUniqueInputType, filterInputType, sortInputType } from './filtering.js';
// eslint-disable-next-line
const deprecated1 = `# Deprecated (use 'filter/id' fields instead).`;
// eslint-disable-next-line
const deprecated2 = `# Deprecated (use 'filter/id' fields instead).`;
const singleReturnProperty = 'result';
const multiReturnProperty = 'results';
/* ------------------------------------- Query Types ------------------------------------- */
/*
A query for a single document
movie(input: SingleMovieInput) : SingleMovieOutput
*/
export const singleQueryType = typeName => Utils.camelCaseify(typeName);
export const singleQueryTemplate = ({ typeName }) =>
`${singleQueryType(typeName)}(input: ${singleInputType(typeName, true)}): ${singleOutputType(typeName)}`;
/*
A query for multiple documents
movies(input: MultiMovieInput) : MultiMovieOutput
*/
export const multiQueryType = typeName => Utils.camelCaseify(Utils.pluralize(typeName));
export const multiQueryTemplate = ({ typeName }) =>
`${multiQueryType(typeName)}(input: ${multiInputType(typeName, false)}): ${multiOutputType(typeName)}`;
/* ------------------------------------- Query Input Types ------------------------------------- */
/*
The argument type when querying for a single document
type SingleMovieInput {
filter: MovieFilterInput
sort: MovieSortInput
search: String
enableCache: Boolean
}
*/
export const singleInputType = (typeName, nonNull = false) => `Single${typeName}Input${nonNull ? '!' : ''}`;
export const singleInputTemplate = ({ typeName }) =>
`input ${singleInputType(typeName)} {
# filtering
filter: ${filterInputType(typeName)}
sort: ${sortInputType(typeName)}
search: String
id: String
# backwards-compatibility
${deprecated1}
selector: ${selectorUniqueInputType(typeName)}
# options (backwards-compatibility)
# Whether to enable caching for this query
enableCache: Boolean
# Return null instead of throwing MissingDocumentError
allowNull: Boolean
# An identifier to name the query's execution context
contextName: String
}`;
/*
The argument type when querying for multiple documents
type MultiMovieInput {
terms: JSON
offset: Int
limit: Int
enableCache: Boolean
}
*/
export const multiInputType = (typeName, nonNull = false) => `Multi${typeName}Input${nonNull ? '!' : ''}`;
export const multiInputTemplate = ({ typeName }) =>
`input ${multiInputType(typeName)} {
# filtering
filter: ${filterInputType(typeName)}
sort: ${sortInputType(typeName)}
search: String
offset: Int
limit: Int
# backwards-compatibility
# A JSON object that contains the query terms used to fetch data
${deprecated2}
terms: JSON
# options (backwards-compatibility)
# Whether to enable caching for this query
enableCache: Boolean
# Whether to calculate totalCount for this query
enableTotal: Boolean
# An identifier to name the query's execution context
contextName: String
}`;
/* ------------------------------------- Query Output Types ------------------------------------- */
/*
The type for the return value when querying for a single document
type SingleMovieOuput{
result: Movie
}
*/
export const singleOutputType = typeName => `Single${typeName}Output`;
export const singleOutputTemplate = ({ typeName }) =>
`type ${singleOutputType(typeName)}{
${singleReturnProperty}: ${typeName}
}`;
/*
The type for the return value when querying for multiple documents
type MultiMovieOuput{
results: [Movie]
totalCount: Int
}
*/
export const multiOutputType = typeName => ` Multi${typeName}Output`;
export const multiOutputTemplate = ({ typeName }) =>
`type ${multiOutputType(typeName)}{
${multiReturnProperty}: [${typeName}]
totalCount: Int
}`;
/* ------------------------------------- Query Queries ------------------------------------- */
/*
Single query used on the client
query singleMovieQuery($input: SingleMovieInput) {
movie(input: $input) {
result {
_id
name
__typename
}
__typename
}
}
*/
// TODO: with hooks, extraQueries becomes less necessary?
export const singleClientTemplate = ({ typeName, fragmentName, extraQueries }) =>
`query ${singleQueryType(typeName)}($input: ${singleInputType(typeName, true)}) {
${singleQueryType(typeName)}(input: $input) {
${singleReturnProperty} {
...${fragmentName}
}
__typename
}
${extraQueries ? extraQueries : ''}
}`;
/*
Multi query used on the client
mutation multiMovieQuery($input: MultiMovieInput) {
movies(input: $input) {
results {
_id
name
__typename
}
totalCount
__typename
}
}
*/
export const multiClientTemplate = ({ typeName, fragmentName, extraQueries }) =>
`query ${multiQueryType(typeName)}($input: ${multiInputType(typeName, false)}) {
${multiQueryType(typeName)}(input: $input) {
${multiReturnProperty} {
...${fragmentName}
}
totalCount
__typename
}
${extraQueries ? extraQueries : ''}
}`;
================================================
FILE: packages/vulcan-lib/lib/modules/graphql_templates/types.js
================================================
export const convertToGraphQL = (fields, indentation) => {
return fields.length > 0 ? fields.map(f => fieldTemplate(f, indentation)).join('\n') : '';
};
export const arrayToGraphQL = fields => fields.map(f => `${f.name}: ${f.type}`).join(', ');
/*
For backwards-compatibility reasons, args can either be a string or an array of objects
*/
export const getArguments = args => {
if (Array.isArray(args) && args.length > 0) {
return `(${arrayToGraphQL(args)})`;
} else if (typeof args === 'string') {
return `(${args})`;
} else {
return '';
}
};
/* ------------------------------------- Generic Field Template ------------------------------------- */
// export const fieldTemplate = ({ name, type, args, directive, description, required }, indentation = '') =>
// `${description ? `${indentation}# ${description}\n` : ''}${indentation}${name}${getArguments(args)}: ${type}${required ? '!' : ''} ${directive ? directive : ''}`;
// version that does not make any fields required
export const fieldTemplate = ({ name, type, args, directive, description, required }, indentation = '') =>
`${description ? `${indentation}# ${description}\n` : ''}${indentation}${name}${getArguments(args)}: ${type} ${
directive ? directive : ''
}`;
/* ------------------------------------- Main Type ------------------------------------- */
/*
The main type
type Movie{
_id: String
title: String
description: String
createdAt: Date
}
*/
export const mainTypeTemplate = ({ typeName, description, interfaces, fields }) =>
`${description ? `# ${description}` : ''}
type ${typeName} ${interfaces.length ? `implements ${interfaces.join(' & ')} ` : ''}{
${convertToGraphQL(fields, ' ')}
}
`;
================================================
FILE: packages/vulcan-lib/lib/modules/handleOptions.js
================================================
/** Helpers to get values depending on name
* E.g. retrieving a collection and its name when only one value is provided
*
*/
import { getCollection } from './collections';
import { getFragment, getFragmentName } from './fragments';
/**
* Extract collectionName from collection
* or collection from collectionName
* @param {*} param0
*/
export const extractCollectionInfo = ({ collectionName, collection }) => {
if (!(collectionName || collection)) throw new Error('Please specify either collection or collectionName');
const _collectionName = collectionName || collection.options.collectionName;
const _collection = collection || getCollection(collectionName);
return { collection: _collection, collectionName: _collectionName };
};
/**
* Extract fragmentName from fragment
* or fragment from fragmentName
*/
export const extractFragmentInfo = ({ fragment, fragmentName }, collectionName) => {
if (!(fragment || fragmentName || collectionName))
throw new Error('Please specify either fragment or fragmentName, or pass a collectionName');
if (fragment) {
return {
fragment,
fragmentName: fragmentName || getFragmentName(fragment)
};
} else {
const _fragmentName = fragmentName || `${collectionName}DefaultFragment`;
return {
fragment: getFragment(_fragmentName),
fragmentName: _fragmentName
};
}
};
================================================
FILE: packages/vulcan-lib/lib/modules/headtags.js
================================================
export const Head = {
meta: [],
link: [],
script: [],
components: [],
};
export const removeFromHeadTags = (type, name)=>{
Head[type] = Head[type].filter((tag)=>{
return (!tag.name || tag.name && tag.name !== name);
});
return Head;
};
================================================
FILE: packages/vulcan-lib/lib/modules/icons.js
================================================
// TODO: get rid of this?
/*
Utilities for displaying icons.
*/
import { Utils } from './utils.js';
// ------------------------------ Dynamic Icons ------------------------------ //
/**
* @summary Take an icon name (such as "open") and return the HTML code to display the icon
* @param {string} iconName - the name of the icon
* @param {string} [iconClass] - an optional class to assign to the icon
*/
Utils.getIcon = function (iconName, iconClass) {
var icons = Utils.icons;
var iconCode = !!icons[iconName] ? icons[iconName] : iconName;
iconClass = (typeof iconClass === 'string') ? ' '+iconClass : '';
return '';
};
/**
* @summary A directory of icon keys and icon codes
*/
Utils.icons = {
expand: 'angle-right',
collapse: 'angle-down',
next: 'angle-right',
close: 'times',
upvote: 'chevron-up',
voted: 'check',
downvote: 'chevron-down',
facebook: 'facebook-square',
twitter: 'twitter',
googleplus: 'google-plus',
linkedin: 'linkedin-square',
comment: 'comment-o',
share: 'share-square-o',
more: 'ellipsis-h',
menu: 'bars',
subscribe: 'envelope-o',
delete: 'trash-o',
edit: 'pencil',
popularity: 'fire',
time: 'clock-o',
best: 'star',
search: 'search',
approve: 'check-circle-o',
reject: 'times-circle-o',
views: 'eye',
clicks: 'mouse-pointer',
score: 'line-chart',
reply: 'reply',
spinner: 'spinner',
new: 'plus',
user: 'user',
like: 'heart',
image: 'picture-o',
};
================================================
FILE: packages/vulcan-lib/lib/modules/index.js
================================================
// import './utils.js';
// import './callbacks.js';
// import './settings.js';
// import './collections.js';
import './deep.js';
import './deep_extend.js';
// import './intl_polyfill.js';
// import './graphql.js';
import './icons.js';
export * from './config';
export * from './graphql/';
export * from './graphql_templates/index.js';
export * from './components.js';
export * from './collections.js';
export * from './callbacks.js';
export * from './routes.js';
export * from './utils.js';
export * from './settings.js';
export * from './headtags.js';
export * from './fragments.js';
export * from './apollo-common';
export * from './dynamic_loader.js';
export * from './admin.js';
export * from './fragment_matcher.js';
export * from './debug.js';
export * from './startup.js';
export * from './errors.js';
export * from './intl.js';
export * from './validation.js';
export * from './handleOptions.js';
export * from './ui_utils.js';
export * from './schema_utils.js';
export * from './simpleSchema_utils.js';
// export * from './resolvers.js';
export * from './random_id.js';
export * from './mongoParams';
export * from './reactive-state.js';
export * from './compose.js';
================================================
FILE: packages/vulcan-lib/lib/modules/intl.js
================================================
import React from 'react';
import SimpleSchema from 'simpl-schema';
import { getSetting } from './settings';
import { debug, Utils } from 'meteor/vulcan:lib';
export const defaultLocale = getSetting('locale', 'en-US');
export const Strings = {};
export const Domains = {};
export const addStrings = (localeId, strings) => {
if (typeof Strings[localeId] === 'undefined') {
Strings[localeId] = {};
}
Strings[localeId] = {
...Strings[localeId],
...strings,
};
};
export const getString = ({ id, values, defaultMessage, messages, locale }) => {
let message = '';
if (messages && messages[id]) {
// first, look in messages object passed through arguments
// note: if defined, messages should also contain Strings[locale]
message = messages[id];
} else if (Strings[locale] && Strings[locale][id]) {
// then look in bundled Strings object
message = Strings[locale][id];
} else if (Strings[defaultLocale] && Strings[defaultLocale][id]) {
// debug(`\x1b[32m>> INTL: No string found for id "${id}" in locale "${locale}", using defaultLocale "${defaultLocale}".\x1b[0m`);
message = Strings[defaultLocale] && Strings[defaultLocale][id];
} else if (defaultMessage) {
// debug(`\x1b[32m>> INTL: No string found for id "${id}" in locale "${locale}", using default message "${defaultMessage}".\x1b[0m`);
message = defaultMessage;
}
if (values && typeof values === 'object' && typeof message === 'string') {
message = pluralizeString(message, values);
message = substituteStringValues(message, values);
}
return message;
};
export const getStrings = localeId => {
return Strings[localeId];
};
/**
* Pluralize a string using [ICU Message syntax used by react-intl](https://formatjs.io/docs/core-concepts/icu-syntax/#plural-format).
* Note: `few` and `many` categories are not supported.
*
* @param {string} message
* @param {object} values
* @return {string}
*/
export const pluralizeString = (message, values) => {
const results = message.match(/{[^,]+, plural, .+?}}/g);
if (!results || !values) {
return message;
}
let pluralizedMessage = message;
for (let result of results) {
const parts = result.replace(/^{|}$/g, '').split(', ');
const key = parts[0];
const value = values[key];
const matches = parts[2].replace(/}$/, '').split('} ');
let translation;
for (const match of matches) {
const category = match.split(' {')[0];
if (
(category === 'zero' && value === 0) ||
(category === 'one' && value === 1) ||
(category === 'two' && value === 2) ||
(category.startsWith('=') && parseInt(category.replace(/^=/, '')) === value) ||
category === 'other'
) {
const phrase = match.split(' {')[1];
translation = phrase.replace('#', value);
break;
}
}
pluralizedMessage = pluralizedMessage.replace(result, translation);
}
return pluralizedMessage;
};
/**
* Substitute values in a message using [react-intl Simple Argument syntax](https://formatjs.io/docs/core-concepts/icu-syntax/#simple-argument)
*
* @param {string} message
* @param {object} values Object with keys that may contain string, number, and React Node values
* @return {string|React.ReactNodeArray} If `values` only contains string and/or number values, a string is returned,
* otherwise an array of React Nodes is returned; both types of results can be used in the same way in a .jsx file
*/
export const substituteStringValues = (message, values) => {
let messageArray = [message];
Object.keys(values).forEach(key => {
const value = values[key];
messageArray = messageArray.reduce((accumulator, message) => {
if (typeof message !== 'string') {
// if this message array element is not a string, pass it on without substituting values
accumulator.push(message);
} else if (typeof value === 'string' || typeof value === 'number') {
// if this value is a string or a number, substitute it
accumulator.push(message.replaceAll(`{${key}}`, value));
} else {
// if this value is a node, break this message array element into three parts:
// 1) the text before the pattern; 2) the React Node; 3) the text after the pattern
const parts = message.split(new RegExp(`{${key}}`, 'g'));
parts.forEach((part, index, array) => {
accumulator.push(part);
if (index < array.length - 1) {
accumulator.push(value);
}
});
}
return accumulator;
}, []);
});
if (messageArray.length === 1) {
// if there is only one array element, it's just a simple string
messageArray = messageArray[0];
} else {
// filter out empty array elements
messageArray = messageArray.reduce((accumulator, message, index) => {
if (typeof message === 'string' && message.length) {
// pass on non-empty string elements
accumulator.push(message);
} else if (!!message) {
// pass on node elements augmented with a `key` prop (required for node arrays)
accumulator.push(React.cloneElement(message, { key: index }));
}
return accumulator;
}, []);
}
return messageArray;
};
export const registerDomain = (locale, domain) => {
Domains[domain] = locale;
};
export const Locales = [];
export const registerLocale = locale => {
Locales.push(locale);
};
// TODO: add support for dynamically loaded locales here
export const getLocale = (localeId) => {
const locales = Locales;
return locales.find(locale => locale.id === localeId);
};
/*
Helper to detect current browser locale
*/
export const detectLocale = () => {
let lang;
if (typeof navigator === 'undefined') {
return null;
}
if (navigator.languages && navigator.languages.length) {
// latest versions of Chrome and Firefox set this correctly
lang = navigator.languages[0];
} else if (navigator.userLanguage) {
// IE only
lang = navigator.userLanguage;
} else {
// latest versions of Chrome, Firefox, and Safari set this correctly
lang = navigator.language;
}
return lang;
};
/*
Figure out the correct locale to use based on the current user, cookies,
and browser settings
*/
export const initLocale = ({ currentUser = {}, cookies = {}, locale }) => {
let userLocaleId = '';
let localeMethod = '';
const detectedLocale = detectLocale();
if (locale) {
// 1. locale is passed from AppGenerator through SSR process
userLocaleId = locale;
localeMethod = 'SSR';
} else if (cookies.locale) {
// 2. look for a cookie
userLocaleId = cookies.locale;
localeMethod = 'cookie';
} else if (currentUser && currentUser.locale) {
// 3. if user is logged in, check for their preferred locale
userLocaleId = currentUser.locale;
localeMethod = 'user';
} else if (detectedLocale) {
// 4. else, check for browser settings
userLocaleId = detectedLocale;
localeMethod = 'browser';
}
/*
NOTE: locale fallback doesn't work anymore because we can now load locales dynamically
and Strings[userLocale] will then be empty
*/
// if user locale is available, use it; else compare first two chars
// of user locale with first two chars of available locales
// const availableLocales = Object.keys(Strings);
// const availableLocale = Strings[userLocale] ? userLocale : availableLocales.find(locale => locale.slice(0, 2) === userLocale.slice(0, 2));
const validLocale = getValidLocale(userLocaleId);
// 4. if user-defined locale is available, use it; else default to setting or `en-US`
if (validLocale) {
return { id: validLocale.id, originalId: userLocaleId, method: localeMethod };
} else {
return { id: getSetting('locale', 'en-US'), originalId: userLocaleId, method: 'setting' };
}
};
/*
Find best matching locale
en-US -> en-US
en-us -> en-US
en-gb -> en-US
etc.
*/
export const truncateKey = key => key.split('-')[0];
export const getValidLocale = localeId => {
const validLocale = Locales.find(locale => {
const { id } = locale;
return id.toLowerCase() === localeId.toLowerCase() || truncateKey(id) === truncateKey(localeId);
});
return validLocale;
};
/*
Look for type name in a few different places
Note: look into simplifying this
*/
export const isIntlField = fieldSchema => !!fieldSchema.intl;
/*
Look for type name in a few different places
Note: look into simplifying this
*/
export const isIntlDataField = fieldSchema => !!fieldSchema.isIntlData;
/*
Check if a schema already has a corresponding intl field
*/
export const schemaHasIntlField = (schema, fieldName) => !!schema[`${fieldName}_intl`];
/*
Generate custom IntlString SimpleSchema type
*/
export const getIntlString = () => {
const schema = {
locale: {
type: String,
optional: true,
},
value: {
type: String,
optional: true,
},
};
const IntlString = new SimpleSchema(schema);
IntlString.name = 'IntlString';
return IntlString;
};
/*
Check if a schema has at least one intl field
*/
export const schemaHasIntlFields = schema => Object.keys(schema).some(fieldName => isIntlField(schema[fieldName]));
/*
Custom validation function to check for required locales
See https://github.com/aldeed/simple-schema-js#custom-field-validation
*/
export const validateIntlField = function() {
let errors = [];
// go through locales to check which one are required
const requiredLocales = Locales.filter(locale => locale.required);
requiredLocales.forEach((locale, index) => {
const strings = this.value;
const hasString = strings && Array.isArray(strings) && strings.some(s => s && s.locale === locale.id && s.value);
if (!hasString) {
const originalFieldName = this.key.replace('_intl', '');
errors.push({
id: 'errors.required',
path: `${this.key}.${index}`,
properties: { name: originalFieldName, locale: locale.id },
});
}
});
if (errors.length > 0) {
// hack to work around the fact that custom validation function can only return a single string
return `intlError|${JSON.stringify(errors)}`;
}
};
/*
Get an array of intl keys to try for a field
*/
export const getIntlKeys = ({ fieldName, collectionName, schema }) => {
const fieldSchema = (schema && schema[fieldName]) || {};
const { intlId } = fieldSchema;
const intlKeys = [];
if (intlId) {
intlKeys.push(intlId);
}
if (collectionName) {
intlKeys.push(`${collectionName.toLowerCase()}.${fieldName}`);
}
intlKeys.push(`global.${fieldName}`);
intlKeys.push(fieldName);
return intlKeys;
};
/**
* getIntlLabel - Get a label for a field, for a given collection, in the current language.
* The evaluation is as follows :
* i18n(intlId) >
* i18n(collectionName.fieldName) >
* i18n(global.fieldName) >
* i18n(fieldName)
*
* @param {object} params
* @param {object} params.intl An intlShape object obtained from the react context for example
* @param {string} params.fieldName The name of the field to evaluate (required)
* @param {string} params.collectionName The name of the collection the field belongs to
* @param {object} params.schema The schema of the collection
* @param {object} values The values to pass to format the i18n string
* @return {string} The translated label
*/
export const getIntlLabel = ({ intl, fieldName, collectionName, schema, isDescription }, values) => {
if (!fieldName) {
throw new Error('fieldName option passed to formatLabel cannot be empty or undefined');
}
// if this is a description, just add .description at the end of the intl key
const suffix = isDescription ? '.description' : '';
const intlKeys = getIntlKeys({ fieldName, collectionName, schema });
let intlLabel;
for (const intlKey of intlKeys) {
const intlString = intl.formatMessage({ id: intlKey + suffix }, values);
if (intlString !== '') {
intlLabel = intlString;
break;
}
}
return intlLabel;
};
/*
Get intl label or fallback
*/
export const formatLabel = (options, values) => {
const { fieldName, schema } = options;
const fieldSchema = (schema && schema[fieldName]) || {};
const { label: schemaLabel } = fieldSchema;
return getIntlLabel(options, values) || schemaLabel || Utils.camelToSpaces(fieldName);
};
================================================
FILE: packages/vulcan-lib/lib/modules/intl_polyfill.js
================================================
/*
intl polyfill. See https://github.com/andyearnshaw/Intl.js/
*/
import { getSetting } from './settings.js';
import areIntlLocalesSupported from 'intl-locales-supported'
var localesMyAppSupports = [
getSetting('locale', 'en-US')
];
if (global.Intl) {
// Determine if the built-in `Intl` has the locale data we need.
if (!areIntlLocalesSupported(localesMyAppSupports)) {
// `Intl` exists, but it doesn't have the data we need, so load the
// polyfill and replace the constructors with need with the polyfill's.
var IntlPolyfill = require('intl');
Intl.NumberFormat = IntlPolyfill.NumberFormat;
Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat;
}
} else {
// No `Intl`, so use and load the polyfill.
global.Intl = require('intl');
}
================================================
FILE: packages/vulcan-lib/lib/modules/mongoParams.js
================================================
/**
* Converts selector and options to Mongo parameters (selector, fields)
*/
import mapValues from 'lodash/mapValues';
import uniq from 'lodash/uniq';
import isEmpty from 'lodash/isEmpty';
import escapeStringRegexp from 'escape-string-regexp';
import merge from 'lodash/merge';
import { Utils } from './utils';
import { getSetting } from './settings.js';
// convert GraphQL selector into Mongo-compatible selector
// TODO: add support for more than just documentId/_id and slug, potentially making conversion unnecessary
// see https://github.com/VulcanJS/Vulcan/issues/2000
export const convertSelector = selector => {
return selector;
};
export const convertUniqueSelector = selector => {
if (selector.documentId) {
selector._id = selector.documentId;
delete selector.documentId;
}
return selector;
};
// see https://stackoverflow.com/a/3561711
export const escapeRegex = s => s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
/*
Filtering
Note: we use $elemMatch syntax for consistency so that we can be sure that every mongo operator function
returns an object.
*/
const conversionTable = {
_eq: '$eq',
_gt: '$gt',
_gte: '$gte',
_in: '$in',
_lt: '$lt',
_lte: '$lte',
_neq: '$ne',
_nin: '$nin',
_is_null: value => ({ $exists: !value }),
_is: value => ({ $elemMatch: { $eq: value } }),
_contains: value => ({ $elemMatch: { $eq: value } }),
_contains_all: '$all',
asc: 1,
desc: -1,
_like: value => ({
$regex: escapeRegex(value),
$options: 'i',
}),
};
// get all fields mentioned in an expression like [ { foo: { _gt: 2 } }, { bar: { _eq : 3 } } ]
const getFieldNames = expressionArray => {
return expressionArray.map(exp => {
const [fieldName] = Object.keys(exp);
return fieldName;
});
};
export const filterFunction = async (collection, input = {}, context) => {
// eslint-disable-next-line no-unused-vars
const { filter, limit, sort, search, filterArguments, offset, id } = input;
let selector = {};
let options = {
sort: {},
};
let filteredFields = [];
const schema = collection.simpleSchema()._schema;
/*
Convert GraphQL expression into MongoDB expression, for example
{ fieldName: { operator: value } }
{ title: { _in: ["foo", "bar"] } }
to:
{ title: { $in: ["foo", "bar"] } }
or (intl fields):
{ title_intl.value: { $in: ["foo", "bar"] } }
*/
const convertExpression = fieldExpression => {
const [fieldName] = Object.keys(fieldExpression);
const operators = Object.keys(fieldExpression[fieldName]);
const mongoExpression = {};
operators.forEach(operator => {
const value = fieldExpression[fieldName][operator];
if (Utils.isEmptyOrUndefined(value)) {
throw new Error(`Detected empty filter value for field “${fieldName}” with operator “${operator}”`);
}
const mongoOperator = conversionTable[operator];
if (!mongoOperator) {
throw new Error(`Operator ${operator} is not valid. Possible operators are: ${Object.keys(conversionTable)}`);
}
const mongoObject = typeof mongoOperator === 'function' ? mongoOperator(value) : { [mongoOperator]: value };
merge(mongoExpression, mongoObject);
});
const isIntl = schema[fieldName].intl;
const mongoFieldName = isIntl ? `${fieldName}_intl.value` : fieldName;
return { [mongoFieldName]: mongoExpression };
};
// id
if (id) {
selector = { _id: id };
}
// filter
if (!isEmpty(filter)) {
Object.keys(filter).forEach(fieldName => {
switch (fieldName) {
case '_and':
filteredFields = filteredFields.concat(getFieldNames(filter._and));
selector['$and'] = filter._and.map(convertExpression);
break;
case '_or':
filteredFields = filteredFields.concat(getFieldNames(filter._or));
selector['$or'] = filter._or.map(convertExpression);
break;
case '_not':
filteredFields = filteredFields.concat(getFieldNames(filter._not));
selector['$not'] = filter._not.map(convertExpression);
break;
case 'search':
break;
default:
const customFilters = collection.options.customFilters;
const customFilter = customFilters && customFilters.find(f => f.name === fieldName);
if (customFilter) {
// field is not actually a field, but a custom filter
const filterArguments = filter[customFilter.name];
// TODO: make this work with await
const filterObject = customFilter.filter({
input,
context,
filterArguments,
});
selector = merge({}, selector, filterObject.selector);
options = merge({}, options, filterObject.options);
} else {
// regular field
filteredFields.push(fieldName);
selector = { ...selector, ...convertExpression({ [fieldName]: filter[fieldName] }) };
}
break;
}
});
}
// sort
if (!isEmpty(sort)) {
options.sort = merge(
{},
options.sort,
mapValues(sort, order => {
const mongoOrder = conversionTable[order];
if (!order) {
throw new Error(`Operator ${order} is not valid. Possible operators: asc, desc`);
}
return mongoOrder;
})
);
} else {
options.sort = { createdAt: -1 }; // reliable default order
}
// search
if (!isEmpty(search)) {
const searchQuery = escapeStringRegexp(search);
const searchableFieldNames = Object.keys(schema).filter(
// do not include intl fields here
fieldName => !fieldName.includes('_intl') && schema[fieldName].searchable
);
if (searchableFieldNames.length) {
selector = {
...selector,
$or: searchableFieldNames.map(fieldName => {
const isIntl = schema[fieldName].intl;
return {
[isIntl ? `${fieldName}_intl.value` : fieldName]: {
$regex: searchQuery,
$options: 'i',
},
};
}),
};
} else {
// eslint-disable-next-line no-console
console.warn(
`Warning: search argument is set but schema ${
collection.options.collectionName
} has no searchable field. Set "searchable: true" for at least one field to enable search.`
);
}
}
// limit
const maxLimit = getSetting('maxDocumentsPerRequest', 1000);
options.limit = limit ? Math.min(limit, maxLimit) : maxLimit;
// offest
if (offset) {
options.skip = offset;
}
// console.log('// collection');
// console.log(collection.options.collectionName);
// console.log('// input');
// console.log(JSON.stringify(input, 2));
// console.log('// selector');
// console.log(JSON.stringify(selector, 2));
// console.log('// options');
// console.log(JSON.stringify(options, 2));
// console.log('// filterFields');
// console.log(uniq(filteredFields));
return {
selector,
options,
filteredFields: uniq(filteredFields),
};
};
================================================
FILE: packages/vulcan-lib/lib/modules/mongo_redux.js
================================================
// TODO: get rid of this?
import Mingo from 'mingo';
Mongo.Collection.prototype.findInStore = function (store, selector = {}, options = {}) {
const typeName = this.options && this.options.typeName;
const docs = _.where(store.getState().apollo.data, {__typename: typeName});
const mingoQuery = new Mingo.Query(selector);
const cursor = mingoQuery.find(docs);
const sortedDocs = cursor.sort(options.sort).all();
// console.log('// findRedux')
// console.log("typeName: ", typeName)
// console.log("selector: ", selector)
// console.log("options: ", options)
// console.log("all docs: ", docs)
// console.log("selected docs: ", cursor.all())
// console.log("sorted docs: ", cursor.sort(options.sort).all())
return {fetch: () => sortedDocs};
};
Mongo.Collection.prototype.findOneInStore = function (store, _idOrObject) {
const docs = typeof _idOrObject === 'string' ? this.findInStore(store, {_id: _idOrObject}).fetch() : this.findInStore(store, _idOrObject).fetch();
return docs.length === 0 ? undefined: docs[0];
};
================================================
FILE: packages/vulcan-lib/lib/modules/random_id.js
================================================
export const Random = {};
import range from 'lodash/range';
import sample from 'lodash/sample';
Random.id = function(length = 17) {
const chars = '23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz';
return range(length)
.map(() => sample(chars))
.join('');
};
================================================
FILE: packages/vulcan-lib/lib/modules/reactive-state.js
================================================
/**
* Simple state management based on Apollo Client reactive variables.
* @see {@link https://www.apollographql.com/docs/react/local-state/reactive-variables/}
* Use it to store session data that survives re-renders and router transitions, unlike component state.
* Register multiple scalar or object states with optional SimpleSchemas for cleaning and validation.
*
* @module reactive-state
*/
import {createSchema} from './schema_utils';
import {makeVar} from '@apollo/client';
// eslint-disable-next-line no-unused-vars
import SimpleSchema from 'simpl-schema';
import _forOwn from 'lodash/forOwn';
const reactiveStates = {};
/**
* An object for storing global state based on Apollo Client reactive variables
* @typedef {function} ReactiveState
* @property {string} stateKey - The name/id/key of the state
* @property {SimpleSchema} [schema] - Optional schema
* @property {*} [defaultValue] - Optional default value
* @property {function} reactiveVar - The reactive variable
*/
/**
* Create a new reactive state
* @param {string} stateKey The name/id/key for the new reactive state
* @param {Object|SimpleSchema} [schema] Optional schema definition object that will be converted to `SimpleSchema`
* using `createSchema()`
* @param {*} [defaultValue] Optional default value; alternatively you can define `defaultValue`s in the schema
* @param {boolean} [skipDuplicate] If you try to create a reactive state with a key that's already used, an exception
* will be thrown; use this option to prevent the exception and use the existing state without changing it
* @returns {ReactiveState} Returns the newly created state object
* @throws Will throw an error if there is already a reactive state with the given key - unless `skipDuplicates` is `true`
*/
export const createReactiveState = ({stateKey, schema, defaultValue, skipDuplicate}) => {
if (reactiveStates[stateKey]) {
if (skipDuplicate) return reactiveStates[stateKey];
throw new Error(`There is already a reactive state named ${stateKey}`);
}
if (schema) {
schema = createSchema(schema);
defaultValue = cleanReactiveStateValue(defaultValue || {}, schema);
}
const reactiveVar = makeVar(defaultValue);
const reactiveState = function (updates) {
let value = reactiveVar();
if (arguments.length > 0) {
if (typeof updates === 'function') {
value = updates(value);
} else if (typeof value === 'object' && typeof updates === 'object') {
value = Object.assign({}, value, updates);
} else if (value === null) {
value = defaultValue;
} else {
value = updates;
}
value = cleanReactiveStateValue(value, schema);
value = reactiveVar(value);
}
return value;
};
reactiveState.stateKey = stateKey;
reactiveState.schema = schema;
reactiveState.defaultValue = defaultValue;
reactiveState.reactiveVar = reactiveVar;
reactiveStates[stateKey] = reactiveState;
return reactiveState;
};
/**
* Return a reactive state previously created
* @param {string} stateKey The key of the desired reactive state
* @returns {ReactiveState}
* @throws Will throw an error if there is no reactive state with the given key
*/
export const getReactiveState = (stateKey) => {
const stateObject = reactiveStates[stateKey];
if (!stateObject) {
throw new Error(`There is no reactive state with stateKey ${stateKey}`);
}
return stateObject;
};
/**
* Given a value to be stored in state, this functions clones, cleans and validates it
* @param {Object} value The value object
* @param {SimpleSchema} [schema] Optional schema for validation
* @returns {Object} The cleaned value
*/
export const cleanReactiveStateValue = (value, schema) => {
if (typeof value === 'object') {
value = {...value};
if (schema) {
value = schema.clean(value);
schema.validate(value);
}
}
return value;
};
/**
* Resets the value of all reactive states to their defaults
*/
export const resetReactiveState = () => {
_forOwn(reactiveStates, function (stateObject, stateKey) {
stateObject(null);
});
};
================================================
FILE: packages/vulcan-lib/lib/modules/routes.js
================================================
import { Components, getComponent } from './components';
export const Routes = {}; // will be populated on startup
export const RoutesTable = {}; // storage for infos about routes themselves
/*
A route is defined in the list like:
RoutesTable.foobar = {
name: 'foobar',
path: '/xyz',
component: getComponent('FooBar')
componentName: 'FooBar' // optional
}
if there there is value for parentRouteName it will look for the route and add the new route as a child of it
*/
export const addRoute = (routeOrRouteArray, options = {}) => {
const { parentRouteName, defaultLayoutComponent } = options;
// be sure to have an array of routes to manipulate
const addedRoutes = Array.isArray(routeOrRouteArray) ? routeOrRouteArray : [routeOrRouteArray];
// if there is a value for parentRouteName you are adding this route as new child
if (parentRouteName) {
addAsChildRoute(parentRouteName, addedRoutes, options);
} else {
// modify the routes table with the new routes
addedRoutes.map(({ name, path, ...properties }) => {
// check if there is already a route registered to this path
const routeWithSamePath = _.findWhere(RoutesTable, { path });
if (routeWithSamePath) {
// delete the route registered with same path
delete RoutesTable[routeWithSamePath.name];
}
const routeObject = {
name,
path,
...properties,
};
if (defaultLayoutComponent && !routeObject.layoutComponent) {
routeObject.layoutComponent = defaultLayoutComponent;
}
// register the new route
RoutesTable[name] = routeObject;
});
}
};
export const extendRoute = (routeName, routeProps) => {
const route = _.findWhere(RoutesTable, { name: routeName });
if (route) {
RoutesTable[route.name] = {
...route,
...routeProps
};
}
};
/**
A route is defined in the list like: (same as above)
RoutesTable.foobar = {
name: 'foobar',
path: '/xyz',
component: getComponent('FooBar')
componentName: 'FooBar' // optional
}
NOTE: This is implemented on single level deep ONLY for now
**/
export const addAsChildRoute = (parentRouteName, addedRoutes) => {
// if the parentRouteName does not exist, error
if (!RoutesTable[parentRouteName]) {
throw new Error(`Route ${parentRouteName} doesn't exist`);
}
// modify the routes table with the new routes
addedRoutes.map(({ name, path, ...properties }) => {
// get the current child routes for this Route
const childRoutes = RoutesTable[parentRouteName]['childRoutes'] || [];
// check if there is already a route registered to this path
const [routeWithSamePath] = _.filter(childRoutes, route => route.path === path);
if (routeWithSamePath) {
// delete the route registered with same path
delete childRoutes[routeWithSamePath.name];
}
// append to the child routes the new route
childRoutes.push({
name,
path,
...properties
});
// register the new child route (overwriting the current which is fine)
RoutesTable[parentRouteName]['childRoutes'] = childRoutes;
});
};
export const getRoute = name => {
const routeDef = RoutesTable[name];
// components should be loaded by now (populateComponentsApp function), we can grab the component in the lookup table and assign it to the route
if (!routeDef.component && routeDef.componentName) {
routeDef.component = getComponent(routeDef.componentName);
}
return routeDef;
};
export const getChildRoute = (name, index) => {
const routeDef = RoutesTable[name]['childRoutes'][index];
// components should be loaded by now (populateComponentsApp function), we can grab the component in the lookup table and assign it to the route
if (!routeDef.component && routeDef.componentName) {
routeDef.component = getComponent(routeDef.componentName);
}
return routeDef;
};
/**
* Populate the lookup table for routes to be callable
* ℹ️ Called once on app startup
**/
export const populateRoutesApp = () => {
// loop over each component in the list
Object.keys(RoutesTable).map(name => {
// loop over child routes if available
if (typeof RoutesTable[name]['childRoutes'] !== typeof undefined) {
RoutesTable[name]['childRoutes'].map((item, index) => {
RoutesTable[name]['childRoutes'][index] = getChildRoute(name, index);
});
}
// populate an entry in the lookup table
Routes[name] = getRoute(name);
// uncomment for debug
// console.log('init route:', name);
});
};
// Should be used only in tests
export const emptyRoutes = () => {
Object.keys(Routes).map((key) => {
delete Routes[key];
});
};
================================================
FILE: packages/vulcan-lib/lib/modules/routes.ts
================================================
import { Components, getComponent } from './components';
export type Route = {
name: string;
path: string;
componentName?: string,
layoutName?: string,
}
export const Routes = new Map(); // will be populated on startup
export const RoutesTable = new Map(); // storage for infos about routes themselves
/*
A route is defined in the list like:
RoutesTable.foobar = {
name: 'foobar',
path: '/xyz',
component: getComponent('FooBar')
componentName: 'FooBar' // optional
}
if there there is value for parentRouteName it will look for the route and add the new route as a child of it
*/
export const addRoute = (routeOrRouteArray: Route|Array, parentRouteName?: string) => {
// be sure to have an array of routes to manipulate
const addedRoutes = Array.isArray(routeOrRouteArray) ? routeOrRouteArray : [routeOrRouteArray];
// if there is a value for parentRouteName you are adding this route as new child
if (parentRouteName) {
addAsChildRoute(parentRouteName, addedRoutes);
} else {
// modify the routes table with the new routes
addedRoutes.forEach(({ name, path, ...properties }) => {
// check if there is already a route registered to this path
const routeWithSamePath = Object.values(RoutesTable).find(route => route.path === path);
if (routeWithSamePath) {
// delete the route registered with same path
delete RoutesTable[routeWithSamePath.name];
}
// register the new route
RoutesTable[name] = {
name,
path,
...properties
};
});
}
};
export const extendRoute = (routeName, routeProps) => {
const route = Object.values(RoutesTable).find(route => route.name === routeName);
if (route) {
RoutesTable[route.name] = {
...route,
...routeProps
};
}
};
/**
A route is defined in the list like: (same as above)
RoutesTable.foobar = {
name: 'foobar',
path: '/xyz',
component: getComponent('FooBar')
componentName: 'FooBar' // optional
}
NOTE: This is implemented on single level deep ONLY for now
**/
export const addAsChildRoute = (parentRouteName, addedRoutes) => {
// if the parentRouteName does not exist, error
if (!RoutesTable[parentRouteName]) {
throw new Error(`Route ${parentRouteName} doesn't exist`);
}
// modify the routes table with the new routes
addedRoutes.map(({ name, path, ...properties }) => {
// get the current child routes for this Route
const childRoutes = RoutesTable[parentRouteName]['childRoutes'] || [];
// check if there is already a route registered to this path
const routeWithSamePath = childRoutes.find(route => route.path === path);
if (routeWithSamePath) {
// delete the route registered with same path
delete childRoutes[routeWithSamePath.name];
}
// append to the child routes the new route
childRoutes.push({
name,
path,
...properties
});
// register the new child route (overwriting the current which is fine)
RoutesTable[parentRouteName]['childRoutes'] = childRoutes;
});
};
export const getRoute = name => {
const routeDef = RoutesTable[name];
// components should be loaded by now (populateComponentsApp function), we can grab the component in the lookup table and assign it to the route
if (!routeDef.component && routeDef.componentName) {
routeDef.component = getComponent(routeDef.componentName);
}
return routeDef;
};
export const getChildRoute = (name, index) => {
const routeDef = RoutesTable[name]['childRoutes'][index];
// components should be loaded by now (populateComponentsApp function), we can grab the component in the lookup table and assign it to the route
if (!routeDef.component && routeDef.componentName) {
routeDef.component = getComponent(routeDef.componentName);
}
return routeDef;
};
/**
* Populate the lookup table for routes to be callable
* ℹ️ Called once on app startup
**/
export const populateRoutesApp = () => {
// loop over each component in the list
Object.keys(RoutesTable).map(name => {
// loop over child routes if available
if (typeof RoutesTable[name]['childRoutes'] !== typeof undefined) {
RoutesTable[name]['childRoutes'].map((item, index) => {
RoutesTable[name]['childRoutes'][index] = getChildRoute(name, index);
});
}
// populate an entry in the lookup table
Routes[name] = getRoute(name);
// uncomment for debug
// console.log('init route:', name);
});
};
// Should be used only in tests
export const emptyRoutes = () => {
Object.keys(Routes).map((key) => {
delete Routes[key];
});
};
================================================
FILE: packages/vulcan-lib/lib/modules/schema_utils.js
================================================
import _reject from 'lodash/reject';
import _keys from 'lodash/keys';
import { Collections } from './collections.js';
import { getNestedSchema, getArrayChild, isBlackbox } from 'meteor/vulcan:lib/lib/modules/simpleSchema_utils';
import _isArray from 'lodash/isArray';
import _get from 'lodash/get';
import _isEmpty from 'lodash/isEmpty';
import _omit from 'lodash/omit';
import SimpleSchema from 'simpl-schema';
import moment from 'moment-timezone';
import { getSetting } from './settings';
export const formattedDateResolver = fieldName => {
return (document = {}, args = {}, context = {}) => {
const { format } = args;
const { timezone = getSetting('timezone') } = context;
if (!document[fieldName]) return;
let m = moment(document[fieldName]);
if (timezone) {
m = m.tz(timezone);
}
return format === 'ago' ? m.fromNow() : m.format(format);
};
};
// extract array items recursively
// first level: foo.$; second level: foo.$.$; etc.
export const extractArrayItems = (schema, fieldName, arrayItem, level = 1) => {
const delimiter = '.$';
const key = fieldName + delimiter.repeat(level);
schema[key] = arrayItem;
if (arrayItem.arrayItem) {
extractArrayItems(schema, fieldName, arrayItem.arrayItem, ++level);
}
};
export const createSchema = (schema, apiSchema = {}, dbSchema = {}) => {
let modifiedSchema = { ...schema };
Object.keys(modifiedSchema).forEach(fieldName => {
const field = schema[fieldName];
const { arrayItem, type, canRead } = field;
if (field.resolveAs) {
// backwards compatibility: copy resolveAs.type to resolveAs.typeName
if (!field.resolveAs.typeName) {
field.resolveAs.typeName = field.resolveAs.type;
}
}
if (field.relation) {
// for now, "translate" new relation field syntax into resolveAs
const { typeName, fieldName, kind } = field.relation;
field.resolveAs = {
typeName,
fieldName,
relation: kind,
};
}
// find any field with an `arrayItem` property defined and add corresponding
// `foo.$` array item field to schema
if (arrayItem) {
extractArrayItems(modifiedSchema, fieldName, arrayItem);
}
// if this is a date field, and field is readable, and fieldFormatted doesn't already exist in the schema
// or as a resolveAs field, then add fieldFormatted to apiSchema
const formattedFieldName = `${fieldName}Formatted`;
if (type === Date && canRead && !schema[formattedFieldName] && !(_get(field, 'resolveAs.fieldName', '') === formattedFieldName)) {
apiSchema[formattedFieldName] = {
typeName: 'String',
canRead,
arguments: 'format: String = "YYYY/MM/DD"',
resolver: formattedDateResolver(fieldName),
};
}
});
// if apiSchema contains fields, copy them over to main schema
if (!_isEmpty(apiSchema)) {
Object.keys(apiSchema).forEach(fieldName => {
const field = apiSchema[fieldName];
const { canRead = ['guests'], description, ...resolveAs } = field;
modifiedSchema[fieldName] = {
type: Object,
optional: true,
apiOnly: true,
canRead,
description,
resolveAs,
};
});
}
// for added security, remove any API-related permission checks from db fields
const filteredDbSchema = {};
const blacklistedFields = ['canRead', 'canCreate', 'canUpdate'];
Object.keys(dbSchema).forEach(dbFieldName => {
filteredDbSchema[dbFieldName] = _omit(dbSchema[dbFieldName], blacklistedFields);
});
// add dbSchema *after* doing the apiSchema stuff so we are sure
// its fields are not exposed through the GraphQL API
modifiedSchema = { ...modifiedSchema, ...filteredDbSchema };
return new SimpleSchema(modifiedSchema);
};
/* getters */
// filter out fields with "." or "$"
export const getValidFields = schema => {
return Object.keys(schema).filter(fieldName => !fieldName.includes('$') && !fieldName.includes('.'));
};
// NOTE: this include fields that should'n't go into the default fragment (pure virtual fields and resolved fields)
// use getFragmentFieldNames for fragments
export const getReadableFields = schema => {
// OpenCRUD backwards compatibility
return getValidFields(schema).filter(fieldName => schema[fieldName].canRead || schema[fieldName].viewableBy);
};
export const getCreateableFields = schema => {
// OpenCRUD backwards compatibility
return getValidFields(schema).filter(fieldName => schema[fieldName].canCreate || schema[fieldName].insertableBy);
};
export const getUpdateableFields = schema => {
// OpenCRUD backwards compatibility
return getValidFields(schema).filter(fieldName => schema[fieldName].canUpdate || schema[fieldName].editableBy);
};
/*
Test if a schema non-nested field should be added to the GraphQL schema or not.
Rule: we always add it except if:
1. addOriginalField: false is specified in one or more resolveAs fields
2. A resolveAs field has the same name as the main field (we don't want two fields with same name)
3. A resolveAs field doesn't have a name (in which case it will take the name of the main field)
*/
export const shouldAddOriginalField = (fieldName, field) => {
if (!field.resolveAs) return true;
const resolveAsArray = Array.isArray(field.resolveAs) ? field.resolveAs : [field.resolveAs];
const removeOriginalField = resolveAsArray.some(
resolveAs => resolveAs.addOriginalField === false || resolveAs.fieldName === fieldName || typeof resolveAs.fieldName === 'undefined'
);
return !removeOriginalField;
};
// list fields that can be included in the default fragment for a schema
export const getFragmentFieldNames = ({ schema, options }) =>
_reject(_keys(schema), fieldName => {
/*
Exclude a field from the default fragment if
1. it has a resolver and original field should not be added
2. it has $ in its name
3. it's not viewable (if onlyViewable option is true)
4. it is not a reference type (typeName is defined for the field or an array child)
*/
const field = schema[fieldName];
// OpenCRUD backwards compatibility
return (
(field.resolveAs && !shouldAddOriginalField(fieldName, field)) ||
fieldName.includes('$') ||
fieldName.includes('.') ||
(options.onlyViewable && !(field.canRead || field.viewableBy)) ||
field.typeName ||
(schema[`${fieldName}.$`] && schema[`${fieldName}.$`].typeName)
);
});
/*
Check if a type corresponds to a collection or else
is just a regular or custom scalar type.
*/
export const isCollectionType = typeName =>
Collections.some(c => c.options.typeName === typeName || `[${c.options.typeName}]` === typeName);
/**
* Iterate over a document fields and run a callback with side effect
* Works recursively for nested fields and arrays of objects (but excluding blackboxed objects, native JSON, and arrays of native values)
* @param {*} document Current document
* @param {*} schema Document schema
* @param {*} callback Called on each field with the corresponding field schema, including fields of nested objects and arrays of nested object
* @param {*} currentPath Global path of the document (to track recursive calls)
* @param {*} isNested Differentiate nested fields
*/
export const forEachDocumentField = (document, schema, callback, currentPath = '') => {
if (!document) return;
Object.keys(document).forEach(fieldName => {
const fieldSchema = schema[fieldName];
callback({ fieldName, fieldSchema, currentPath, document, schema, isNested: !!currentPath });
// Check if we need a recursive call
if (!fieldSchema) return; // field has no corresponding schema, we are done
const value = document[fieldName];
if (!value) return;
// if value is an array, validate permissions for all children
if (_isArray(value)) {
const arrayChildField = getArrayChild(fieldName, schema);
if (arrayChildField) {
const arrayFieldSchema = getNestedSchema(arrayChildField);
// apply only if the field is an array of objects
if (arrayFieldSchema) {
value.forEach((item, idx) => {
forEachDocumentField(item, arrayFieldSchema, callback, `${currentPath}${fieldName}[${idx}].`);
});
}
}
// if value is an object, run recursively
} else if (typeof value === 'object' && !isBlackbox(fieldSchema)) {
const nestedFieldSchema = getNestedSchema(fieldSchema);
if (nestedFieldSchema) {
forEachDocumentField(value, nestedFieldSchema, callback, `${currentPath}${fieldName}.`);
}
}
});
};
================================================
FILE: packages/vulcan-lib/lib/modules/settings.js
================================================
import Vulcan from './config.js';
import flatten from 'flat';
const getNestedProperty = function (obj, desc) {
var arr = desc.split('.');
while(arr.length && (obj = obj[arr.shift()]));
return obj;
};
export const Settings = {};
export const getAllSettings = () => {
const settingsObject = {};
let rootSettings = _.clone(Meteor.settings);
delete rootSettings.public;
delete rootSettings.private;
// root settings & private settings are both private
rootSettings = flatten(rootSettings, {safe: true});
const privateSettings = flatten(Meteor.settings.private || {}, {safe: true});
// public settings
const publicSettings = flatten(Meteor.settings.public || {}, {safe: true});
// registered default values
const registeredSettings = Settings;
const allSettingKeys = _.union(_.keys(rootSettings), _.keys(publicSettings), _.keys(privateSettings), _.keys(registeredSettings));
allSettingKeys.sort().forEach(key => {
settingsObject[key] = {};
if (typeof rootSettings[key] !== 'undefined') {
settingsObject[key].value = rootSettings[key];
} else if (typeof privateSettings[key] !== 'undefined') {
settingsObject[key].value = privateSettings[key];
} else if (typeof publicSettings[key] !== 'undefined') {
settingsObject[key].value = publicSettings[key];
}
if (typeof publicSettings[key] !== 'undefined'){
settingsObject[key].isPublic = true;
}
if (registeredSettings[key]) {
if (registeredSettings[key].defaultValue !== null || registeredSettings[key].defaultValue !== undefined) settingsObject[key].defaultValue = registeredSettings[key].defaultValue;
if (registeredSettings[key].description) settingsObject[key].description = registeredSettings[key].description;
}
});
return _.map(settingsObject, (setting, key) => ({name: key, ...setting}));
};
Vulcan.showSettings = () => {
return getAllSettings();
};
export const registerSetting = (settingName, defaultValue, description, isPublic) => {
Settings[settingName] = { defaultValue, description, isPublic };
};
export const getSetting = (settingName, settingDefault) => {
let setting;
// if a default value has been registered using registerSetting, use it
if (typeof settingDefault === 'undefined' && Settings[settingName])
settingDefault = Settings[settingName].defaultValue;
if (Meteor.isServer) {
// look in public, private, and root
const rootSetting = getNestedProperty(Meteor.settings, settingName);
const privateSetting = Meteor.settings.private && getNestedProperty(Meteor.settings.private, settingName);
const publicSetting = Meteor.settings.public && getNestedProperty(Meteor.settings.public, settingName);
// if setting is an object, "collect" properties from all three places
if (typeof rootSetting === 'object' || typeof privateSetting === 'object' || typeof publicSetting === 'object') {
setting = {
...settingDefault,
...rootSetting,
...privateSetting,
...publicSetting,
};
} else {
if (typeof rootSetting !== 'undefined') {
setting = rootSetting;
} else if (typeof privateSetting !== 'undefined') {
setting = privateSetting;
} else if (typeof publicSetting !== 'undefined') {
setting = publicSetting;
} else {
setting = settingDefault;
}
}
} else {
// look only in public
const publicSetting = Meteor.settings.public && getNestedProperty(Meteor.settings.public, settingName);
setting = typeof publicSetting !== 'undefined' ? publicSetting : settingDefault;
}
// Settings[settingName] = {...Settings[settingName], settingValue: setting};
return setting;
};
registerSetting('debug', false, 'Enable debug mode (more verbose logging)');
================================================
FILE: packages/vulcan-lib/lib/modules/simpleSchema_utils.js
================================================
/**
* Helpers specific to Simple Schema
* See "schema_utils" for more generic methods
*/
// remove ".$" at the end of array child fieldName
export const unarrayfyFieldName = (fieldName) => {
return fieldName ? fieldName.split('.')[0] : fieldName;
};
// allowed values of a field if present
export const getAllowedValues = (field) => field.type.definitions[0].allowedValues;
export const hasAllowedValues = field => {
const allowedValues = getAllowedValues(field);
if (allowedValues && !allowedValues.length) {
console.warn(`Field ${field} as empty allowed values`);
return false;
}
return !!allowedValues;
};
export const isArrayChildField = fieldName => fieldName.indexOf('$') !== -1;
export const isBlackbox = (field) => !!field.type.definitions[0].blackbox;
//export const isBlackbox = (fieldName, schema) => {
// const field = schema[fieldName];
// // for array field, check parent recursively to find a blackbox
// if (isArrayChildField(fieldName)) {
// const parentField = schema[fieldName.slice(0, -2)];
// return isBlackbox(parentField);
// }
// return field.type.definitions[0].blackbox;
//};
export const getFieldType = field => field.type.singleType || field.type[0].type;
export const getFieldTypeName = fieldType =>
typeof fieldType === 'object'
? 'Object'
: typeof fieldType === 'function'
? fieldType.name
: fieldType;
export const getArrayChild = (fieldName, schema) => schema[`${fieldName}.$`];
export const getNestedSchema = field => field.type.singleType._schema;
================================================
FILE: packages/vulcan-lib/lib/modules/startup.js
================================================
import { runCallbacks } from './callbacks';
Meteor.startup(() => {
runCallbacks('app.startup');
});
================================================
FILE: packages/vulcan-lib/lib/modules/ui_utils.js
================================================
import pick from 'lodash/pick';
/**
* Extract input props for the FormComponentInner
* @param {*} props All component props
* @returns Initial props + props specific to the HTML input in an inputProperties object
*/
export const getHtmlInputProps = props => {
const { name, path, options, label, onChange, onBlur, value, disabled } = props;
// these properties are whitelisted so that they can be safely passed to the actual form input
// and avoid https://facebook.github.io/react/warnings/unknown-prop.html warnings
const inputProperties = {
...props.inputProperties,
name,
path,
options,
label,
onChange,
onBlur,
value,
disabled,
};
return {
...props,
inputProperties,
};
};
/**
* Extract input props for the FormComponentInner
* @param {*} props All component props
* @returns Initial props + props specific to the HTML input in an inputProperties object
*/
export const whitelistInputProps = props => {
const whitelist = ['name', 'path', 'options', 'label', 'onChange', 'onBlur', 'value', 'disabled', 'placeholder'];
return pick(props, whitelist);
};
================================================
FILE: packages/vulcan-lib/lib/modules/utils.js
================================================
/*
Utilities
*/
import marked from 'marked';
import urlObject from 'url';
import moment from 'moment';
import getSlug from 'speakingurl';
import { getSetting, registerSetting } from './settings.js';
import { Routes } from './routes.js';
import { getCollection } from './collections.js';
import set from 'lodash/set';
import get from 'lodash/get';
import isFunction from 'lodash/isFunction';
import pluralize from 'pluralize';
import { getFieldType } from './simpleSchema_utils';
import { forEachDocumentField } from './schema_utils';
import isEmpty from 'lodash/isEmpty';
registerSetting('debug', false, 'Enable debug mode (more verbose logging)');
/**
* @summary The global namespace for Vulcan utils.
* @namespace Telescope.utils
*/
export const Utils = {};
/**
* @summary Convert a camelCase string to dash-separated string
* @param {String} str
*/
Utils.camelToDash = function (str) {
return str
.replace(/\W+/g, '-')
.replace(/([a-z\d])([A-Z])/g, '$1-$2')
.toLowerCase();
};
/**
* @summary Convert a camelCase string to a space-separated capitalized string
* See http://stackoverflow.com/questions/4149276/javascript-camelcase-to-regular-form
* @param {String} str
*/
Utils.camelToSpaces = function (str) {
return str.replace(/([A-Z])/g, ' $1').replace(/^./, function (str) {
return str.toUpperCase();
});
};
/**
* @summary Convert a string to title case ('foo bar baz' to 'Foo Bar Baz')
* See https://stackoverflow.com/questions/4878756/how-to-capitalize-first-letter-of-each-word-like-a-2-word-city
* @param {String} str
*/
Utils.toTitleCase = str =>
str &&
str
.toLowerCase()
.split(' ')
.map(s => s.charAt(0).toUpperCase() + s.substring(1))
.join(' ');
/**
* @summary Convert an underscore-separated string to dash-separated string
* @param {String} str
*/
Utils.underscoreToDash = function (str) {
return str.replace('_', '-');
};
/**
* @summary Convert a dash separated string to camelCase.
* @param {String} str
*/
Utils.dashToCamel = function (str) {
return str.replace(/(\-[a-z])/g, function ($1) {
return $1.toUpperCase().replace('-', '');
});
};
/**
* @summary Convert a string to camelCase and remove spaces.
* @param {String} str
*/
Utils.camelCaseify = function (str) {
str = this.dashToCamel(str.replace(' ', '-'));
str = str.slice(0, 1).toLowerCase() + str.slice(1);
return str;
};
/**
* @summary Trim a sentence to a specified amount of words and append an ellipsis.
* @param {String} s - Sentence to trim.
* @param {Number} numWords - Number of words to trim sentence to.
*/
Utils.trimWords = function (s, numWords) {
if (!s) return s;
var expString = s.split(/\s+/, numWords);
if (expString.length >= numWords) return expString.join(' ') + '…';
return s;
};
/**
* @summary Trim a block of HTML code to get a clean text excerpt
* @param {String} html - HTML to trim.
*/
Utils.trimHTML = function (html, numWords) {
var text = Utils.stripHTML(html);
return Utils.trimWords(text, numWords);
};
/**
* @summary Capitalize a string.
* @param {String} str
*/
export const capitalize = function (str) {
return str && str.charAt(0).toUpperCase() + str.slice(1);
};
Utils.capitalize = capitalize;
Utils.t = function (message) {
var d = new Date();
console.log(
'### ' + message + ' rendered at ' + d.getHours() + ':' + d.getMinutes() + ':' + d.getSeconds()
); // eslint-disable-line
};
Utils.nl2br = function (str) {
var breakTag = ' ';
return (str + '').replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1' + breakTag + '$2');
};
Utils.scrollPageTo = function (selector) {
$('body').scrollTop($(selector).offset().top);
};
Utils.scrollIntoView = function (selector) {
if (!document) return;
const element = document.querySelector(selector);
if (element) {
element.scrollIntoView();
}
};
Utils.getDateRange = function (pageNumber) {
var now = moment(new Date());
var dayToDisplay = now.subtract(pageNumber - 1, 'days');
var range = {};
range.start = dayToDisplay.startOf('day').valueOf();
range.end = dayToDisplay.endOf('day').valueOf();
// console.log("after: ", dayToDisplay.startOf('day').format("dddd, MMMM Do YYYY, h:mm:ss a"));
// console.log("before: ", dayToDisplay.endOf('day').format("dddd, MMMM Do YYYY, h:mm:ss a"));
return range;
};
//////////////////////////
// URL Helper Functions //
//////////////////////////
/**
* @summary Returns the user defined site URL or Meteor.absoluteUrl. Add trailing '/' if missing
*/
Utils.getSiteUrl = function (addSlash = true) {
let url = getSetting('siteUrl', Meteor.absoluteUrl());
if (url.slice(-1) !== '/' && addSlash) {
url += '/';
}
return url;
};
/**
* @summary Returns the user defined site URL or Meteor.absoluteUrl. Remove trailing '/' if it exists
*/
Utils.getRootUrl = function () {
let url = getSetting('siteUrl', Meteor.absoluteUrl());
if (url.slice(-1) === '/') {
url = url.slice(0, -1);
}
return url;
};
/**
* @summary The global namespace for Vulcan utils.
* @param {String} url - the URL to redirect
*/
Utils.getOutgoingUrl = function (url) {
return Utils.getSiteUrl() + 'out?url=' + encodeURIComponent(url);
};
Utils.slugify = function (s) {
let slug = getSlug(s, {
truncate: 60,
});
// can't have posts with an "edit" slug
if (slug === 'edit') {
slug = 'edit-1';
}
return slug;
};
/**
* @summary Given a collection and a slug, returns the same or modified slug that's unique within the collection;
* It's modified by appending a dash and an integer; eg: my-slug => my-slug-1
* @param {Object} collection
* @param {string} slug
* @param {string} [documentId] If you are generating a slug for an existing document, pass it's _id to
* avoid the slug changing
* @returns {string} The slug passed in the 2nd param, but may be
*/
Utils.getUnusedSlug = function (collection, slug, documentId) {
// test if slug is already in use
for (let index = 0; index <= Number.MAX_SAFE_INTEGER; index++) {
const suffix = index ? '-' + index : '';
const documentWithSlug = collection.findOne({ slug: slug + suffix });
if (!documentWithSlug || (documentId && documentWithSlug._id === documentId)) {
return slug + suffix;
}
}
};
// Different version, less calls to the db but it cannot be used until we figure out how to use async for onCreate functions
// Utils.getUnusedSlug = async function (collection, slug) {
// let suffix = '';
// let index = 0;
//
// const slugRegex = new RegExp('^' + slug + '-[0-9]+$');
// // get all the slugs matching slug or slug-123 in that collection
// const results = await collection.find( { slug: { $in: [slug, slugRegex] } }, { fields: { slug: 1, _id: 0 } });
// const usedSlugs = results.map(item => item.slug);
// // increment the index at the end of the slug until we find an unused one
// while (usedSlugs.indexOf(slug + suffix) !== -1) {
// index++;
// suffix = '-' + index;
// }
// return slug + suffix;
// };
/**
* @summary This is the same as Utils.getUnusedSlug(), but takes the name of the collection instead
* @param {string} collectionName
* @param {string} slug
* @param {string} [documentId]
* @returns {string}
*/
Utils.getUnusedSlugByCollectionName = function (collectionName, slug, documentId) {
return Utils.getUnusedSlug(getCollection(collectionName), slug, documentId);
};
Utils.getShortUrl = function (post) {
return post.shortUrl || post.url;
};
Utils.getDomain = function (url) {
try {
return urlObject.parse(url).hostname.replace('www.', '');
} catch (error) {
return null;
}
};
// add http: if missing
Utils.addHttp = function (url) {
try {
if (url.substring(0, 5) !== 'http:' && url.substring(0, 6) !== 'https:') {
url = 'http:' + url;
}
return url;
} catch (error) {
return null;
}
};
/////////////////////////////
// String Helper Functions //
/////////////////////////////
Utils.cleanUp = function (s) {
return this.stripHTML(s);
};
Utils.sanitize = function (s) {
return s;
};
Utils.stripHTML = function (s) {
return s && s.replace(/<(?:.|\n)*?>/gm, '');
};
Utils.stripMarkdown = function (s) {
var htmlBody = marked(s);
return Utils.stripHTML(htmlBody);
};
// http://stackoverflow.com/questions/2631001/javascript-test-for-existence-of-nested-object-key
Utils.checkNested = function (obj /*, level1, level2, ... levelN*/) {
var args = Array.prototype.slice.call(arguments);
obj = args.shift();
for (var i = 0; i < args.length; i++) {
if (!obj.hasOwnProperty(args[i])) {
return false;
}
obj = obj[args[i]];
}
return true;
};
Utils.log = function (s) {
if (getSetting('debug', false) || process.env.NODE_ENV === 'development') {
console.log(s); // eslint-disable-line
}
};
// see http://stackoverflow.com/questions/8051975/access-object-child-properties-using-a-dot-notation-string
Utils.getNestedProperty = function (obj, desc) {
var arr = desc.split('.');
while (arr.length && (obj = obj[arr.shift()]));
return obj;
};
// see http://stackoverflow.com/a/14058408/649299
_.mixin({
compactObject: function (object) {
var clone = _.clone(object);
_.each(clone, function (value, key) {
/*
Remove a value if:
1. it's not a boolean
2. it's not a number
3. it's undefined
4. it's an empty string
5. it's null
6. it's an empty array
*/
if (typeof value === 'boolean' || typeof value === 'number') {
return;
}
if (
value === undefined ||
value === null ||
value === '' ||
(Array.isArray(value) && value.length === 0)
) {
delete clone[key];
}
});
return clone;
},
});
Utils.getFieldLabel = (fieldName, collection) => {
const label = collection.simpleSchema()._schema[fieldName].label;
const nameWithSpaces = Utils.camelToSpaces(fieldName);
return label || nameWithSpaces;
};
Utils.getLogoUrl = () => {
const logoUrl = getSetting('logoUrl');
if (logoUrl) {
const prefix = Utils.getSiteUrl().slice(0, -1);
// the logo may be hosted on another website
return logoUrl.indexOf('://') > -1 ? logoUrl : prefix + logoUrl;
}
};
Utils.findIndex = (array, predicate) => {
let index = -1;
let continueLoop = true;
array.forEach((item, currentIndex) => {
if (continueLoop && predicate(item)) {
index = currentIndex;
continueLoop = false;
}
});
return index;
};
// adapted from http://stackoverflow.com/a/22072374/649299
Utils.unflatten = function (array, options, parent, level = 0, tree) {
const {
idProperty = '_id',
parentIdProperty = 'parentId',
childrenProperty = 'childrenResults',
} = options;
level++;
tree = typeof tree !== 'undefined' ? tree : [];
let children = [];
if (typeof parent === 'undefined') {
// if there is no parent, we're at the root level
// so we return all root nodes (i.e. nodes with no parent)
children = _.filter(array, node => !get(node, parentIdProperty));
} else {
// if there *is* a parent, we return all its child nodes
// (i.e. nodes whose parentId is equal to the parent's id.)
children = _.filter(array, node => get(node, parentIdProperty) === get(parent, idProperty));
}
// if we found children, we keep on iterating
if (!!children.length) {
if (typeof parent === 'undefined') {
// if we're at the root, then the tree consist of all root nodes
tree = children;
} else {
// else, we add the children to the parent as the "childrenResults" property
set(parent, childrenProperty, children);
}
// we call the function on each child
children.forEach(child => {
child.level = level;
Utils.unflatten(array, options, child, level);
});
}
return tree;
};
// remove the telescope object from a schema and duplicate it at the root
Utils.stripTelescopeNamespace = schema => {
// grab the users schema keys
const schemaKeys = Object.keys(schema);
// remove any field beginning by telescope: .telescope, .telescope.upvotedPosts.$, ...
const filteredSchemaKeys = schemaKeys.filter(key => key.slice(0, 9) !== 'telescope');
// replace the previous schema by an object based on this filteredSchemaKeys
return filteredSchemaKeys.reduce((sch, key) => ({ ...sch, [key]: schema[key] }), {});
};
/**
* Get the display name of a React component
* @param {React Component} WrappedComponent
*/
Utils.getComponentDisplayName = WrappedComponent => {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
};
/**
* Take a collection and a list of documents, and convert all their date fields to date objects
* This is necessary because Apollo doesn't support custom scalars, and stores dates as strings
* @param {Object} collection
* @param {Array} list
*/
Utils.convertDates = (collection, listOrDocument) => {
// if undefined, just return
if (!listOrDocument) return listOrDocument;
const isArray = listOrDocument && Array.isArray(listOrDocument);
if (isArray && !listOrDocument.length) return listOrDocument;
const list = isArray ? listOrDocument : [listOrDocument];
const schema = collection.simpleSchema()._schema;
//Nested version
const convertedList = list.map((document) => {
forEachDocumentField(document, schema, ({ fieldName, fieldSchema, currentPath }) => {
if (fieldSchema && getFieldType(fieldSchema) === Date) {
const valuePath = `${currentPath}${fieldName}`;
const value = get(document, valuePath);
set(document, valuePath, new Date(value));
}
});
return document;
});
return isArray ? convertedList : convertedList[0];
};
Utils.encodeIntlError = error => (typeof error !== 'object' ? error : JSON.stringify(error));
Utils.decodeIntlError = (error, options = { stripped: false }) => {
try {
// do we get the error as a string or as an error object?
let strippedError = typeof error === 'string' ? error : error.message;
// if the error hasn't been cleaned before (ex: it's not an error from a form)
if (!options.stripped) {
// strip the "GraphQL Error: message [error_code]" given by Apollo if present
const graphqlPrefixIsPresent = strippedError.match(/GraphQL error: (.*)/);
if (graphqlPrefixIsPresent) {
strippedError = graphqlPrefixIsPresent[1];
}
// strip the error code if present
const errorCodeIsPresent = strippedError.match(/(.*)\[(.*)\]/);
if (errorCodeIsPresent) {
strippedError = errorCodeIsPresent[1];
}
}
// the error is an object internationalizable
const parsedError = JSON.parse(strippedError);
// check if the error has at least an 'id' expected by react-intl
if (!parsedError.id) {
console.error('[Undecodable error]', error); // eslint-disable-line
return { id: 'app.something_bad_happened', value: '[undecodable error]' };
}
// return the parsed error
return parsedError;
} catch (__) {
// the error is not internationalizable
return error;
}
};
Utils.findWhere = (array, criteria) =>
array.find(item => Object.keys(criteria).every(key => item[key] === criteria[key]));
Utils.defineName = (o, name) => {
Object.defineProperty(o, 'name', { value: name });
return o;
};
Utils.getRoutePath = routeName => {
return Routes[routeName] && Routes[routeName].path;
};
String.prototype.replaceAll = function (search, replacement) {
var target = this;
return target.replace(new RegExp(search, 'g'), replacement);
};
Utils.isPromise = value => isFunction(get(value, 'then'));
/**
* Pluralize helper with clash name prevention (adds an S)
*/
Utils.pluralize = (text, ...args) => {
const res = pluralize(text, ...args);
// avoid edge case like "people" where plural is identical to singular, leading to name clash
// in resolvers
if (res === text) {
return res + 's';
}
return res;
};
Utils.singularize = pluralize.singular;
Utils.removeProperty = (obj, propertyName) => {
for (const prop in obj) {
if (prop === propertyName) {
delete obj[prop];
} else if (typeof obj[prop] === 'object') {
Utils.removeProperty(obj[prop], propertyName);
}
}
};
/**
* Convert an array of field options into an allowedValues array
* @param {Array} schemaFieldOptionsArray
*/
Utils.getSchemaFieldAllowedValues = schemaFieldOptionsArray => {
if (!Array.isArray(schemaFieldOptionsArray)) {
throw new Error('Utils.getAllowedValues: Expected Array');
}
return schemaFieldOptionsArray.map(schemaFieldOption => schemaFieldOption.value);
};
/**
* type is an array due to the possibility of using SimpleSchema.oneOf
* right now we support only fields with one type
* @param {Object} field
*/
Utils.getFieldType = field => get(field, 'type.definitions.0.type');
/**
* Convert an array of field names into a Mongo fields specifier
* @param {Array} fieldsArray
*/
Utils.arrayToFields = fieldsArray => {
return _.object(
fieldsArray,
_.map(fieldsArray, function () {
return true;
})
);
};
Utils.isEmptyOrUndefined = value =>
typeof value === 'undefined' ||
value === null ||
//value === '' ||
(
typeof value === 'object' &&
isEmpty(value) &&
!(value instanceof Date) &&
!(value instanceof RegExp)
);
================================================
FILE: packages/vulcan-lib/lib/modules/validation.js
================================================
import pickBy from 'lodash/pickBy';
import mapValues from 'lodash/mapValues';
import { forEachDocumentField } from './schema_utils';
export const dataToModifier = data => ({
$set: pickBy(data, f => f !== null),
$unset: mapValues(pickBy(data, f => f === null), () => true),
});
export const modifierToData = modifier => ({
...modifier.$set,
...mapValues(modifier.$unset, () => null),
});
/**
* Validate a document permission recursively
* @param {*} fullDocument (must not be partial since permission logic may rely on full document)
* @param {*} documentToValidate document to validate
* @param {*} schema Simple schema
* @param {*} context Current user and Users collectionœ
* @param {*} mode create or update
* @param {*} currentPath current path for recursive calls (nested, nested[0].foo, ...)
*/
const validateDocumentPermissions = (fullDocument, documentToValidate, schema, context, mode = 'create', currentPath = '') => {
let validationErrors = [];
const { Users, currentUser } = context;
forEachDocumentField(documentToValidate, schema,
({ fieldName, fieldSchema, currentPath, isNested }) => {
if (isNested && (!fieldSchema || (mode === 'create' ? !fieldSchema.canCreate : !fieldSchema.canUpdate))) return; // ignore nested without permission
if (!fieldSchema
|| (mode === 'create' ? !Users.canCreateField(currentUser, fieldSchema) : !Users.canUpdateField(currentUser, fieldSchema, fullDocument))
) {
validationErrors.push({
id: 'errors.disallowed_property_detected',
properties: {
name: `${currentPath}${fieldName}`
},
});
}
});
return validationErrors;
};
/*
If document is not trusted, run validation steps:
1. Check that the current user has permission to edit each field
2. Run SimpleSchema validation step
*/
export const validateDocument = (document, collection, context, validationContextName = 'defaultContext') => {
const schema = collection.simpleSchema()._schema;
let validationErrors = [];
// validate creation permissions (and other Vulcan-specific constraints)
validationErrors = validationErrors.concat(
validateDocumentPermissions(document, document, schema, context, 'create')
);
// run simple schema validation (will check the actual types, required fields, etc....)
const validationContext = collection.simpleSchema().namedContext(validationContextName);
validationContext.validate(document);
if (!validationContext.isValid()) {
const errors = validationContext.validationErrors();
errors.forEach(error => {
// eslint-disable-next-line no-console
// console.log(error);
if (error.type.includes('intlError')) {
const intlError = JSON.parse(error.type.replace('intlError|', ''));
validationErrors = validationErrors.concat(intlError);
} else {
validationErrors.push({
id: `errors.${error.type}`,
path: error.name,
properties: {
collectionName: collection.options.collectionName,
typeName: collection.options.typeName,
...error,
},
});
}
});
}
return validationErrors;
};
/*
If document is not trusted, run validation steps:
1. Check that the current user has permission to insert each field
2. Run SimpleSchema validation step
*/
export const validateModifier = (modifier, data, document, collection, context, validationContextName = 'defaultContext') => {
const schema = collection.simpleSchema()._schema;
const set = modifier.$set;
const unset = modifier.$unset;
let validationErrors = [];
// 1. check that the current user has permission to edit each field
validationErrors = validationErrors.concat(
validateDocumentPermissions(document, data, schema, context, 'update')
);
// 2. run SS validation
const validationContext = collection.simpleSchema().namedContext(validationContextName);
validationContext.validate({ $set: set, $unset: unset }, { modifier: true, extendedCustomContext: { documentId: document._id } });
if (!validationContext.isValid()) {
const errors = validationContext.validationErrors();
errors.forEach(error => {
// eslint-disable-next-line no-console
// console.log(error);
if (error.type.includes('intlError')) {
validationErrors = validationErrors.concat(JSON.parse(error.type.replace('intlError|', '')));
} else {
validationErrors.push({
id: `errors.${error.type}`,
path: error.name,
properties: {
collectionName: collection.options.collectionName,
typeName: collection.options.typeName,
...error,
},
});
}
});
}
return validationErrors;
};
export const validateData = (data, document, collection, context) => {
return validateModifier(dataToModifier(data), data, document, collection, context);
};
/*
The following versions were written to be more SimpleSchema-agnostic, but
are not currently used
*/
/*
If document is not trusted, run validation steps:
1. Check that the current user has permission to edit each field
2. Check field lengths
3. Check field types
4. Check for missing fields
5. Run SimpleSchema validation step (for now)
*/
export const validateDocumentNotUsed = (document, collection, context) => {
const { Users, currentUser } = context;
const schema = collection.simpleSchema()._schema;
let validationErrors = [];
// Check validity of inserted document
_.forEach(document, (value, fieldName) => {
const fieldSchema = schema[fieldName];
// 1. check that the current user has permission to insert each field
if (!fieldSchema || !Users.canCreateField(currentUser, fieldSchema)) {
validationErrors.push({
id: 'app.disallowed_property_detected',
fieldName,
});
}
// 2. check field lengths
if (fieldSchema.limit && value.length > fieldSchema.limit) {
validationErrors.push({
id: 'app.field_is_too_long',
data: { fieldName, limit: fieldSchema.limit },
});
}
// 3. check that fields have the proper type
// TODO
});
// 4. check that required fields have a value
_.keys(schema).forEach(fieldName => {
const fieldSchema = schema[fieldName];
if ((fieldSchema.required || !fieldSchema.optional) && typeof document[fieldName] === 'undefined') {
validationErrors.push({
id: 'app.required_field_missing',
data: { fieldName },
});
}
});
// 5. still run SS validation for now for backwards compatibility
try {
collection.simpleSchema().validate(document);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
validationErrors.push({
id: 'app.schema_validation_error',
data: { message: error.message },
});
}
return validationErrors;
};
/*
If document is not trusted, run validation steps:
1. Check that the current user has permission to insert each field
2. Check field lengths
3. Check field types
4. Check for missing fields
5. Run SimpleSchema validation step (for now)
*/
export const validateModifierNotUsed = (modifier, document, collection, context) => {
const { Users, currentUser } = context;
const schema = collection.simpleSchema()._schema;
const set = modifier.$set;
const unset = modifier.$unset;
let validationErrors = [];
// 1. check that the current user has permission to edit each field
const modifiedProperties = _.keys(set).concat(_.keys(unset));
modifiedProperties.forEach(function (fieldName) {
var field = schema[fieldName];
if (!field || !Users.canUpdateField(currentUser, field, document)) {
validationErrors.push({
id: 'app.disallowed_property_detected',
data: { name: fieldName },
});
}
});
// Check validity of set modifier
_.forEach(set, (value, fieldName) => {
const fieldSchema = schema[fieldName];
// 2. check field lengths
if (fieldSchema.limit && value.length > fieldSchema.limit) {
validationErrors.push({
id: 'app.field_is_too_long',
data: { name: fieldName, limit: fieldSchema.limit },
});
}
// 3. check that fields have the proper type
// TODO
});
// 4. check that required fields have a value
// when editing, we only want to require fields that are actually part of the form
// so we make sure required keys are present in the $unset object
_.keys(schema).forEach(fieldName => {
const fieldSchema = schema[fieldName];
if (unset[fieldName] && (fieldSchema.required || !fieldSchema.optional) && typeof set[fieldName] === 'undefined') {
validationErrors.push({
id: 'app.required_field_missing',
data: { name: fieldName },
});
}
});
// 5. still run SS validation for now for backwards compatibility
const validationContext = collection.simpleSchema().newContext();
validationContext.validate({ $set: set, $unset: unset }, { modifier: true });
if (!validationContext.isValid()) {
const errors = validationContext.validationErrors();
errors.forEach(error => {
// eslint-disable-next-line no-console
// console.log(error);
validationErrors.push({
id: 'app.schema_validation_error',
data: error,
});
});
}
return validationErrors;
};
================================================
FILE: packages/vulcan-lib/lib/server/accounts_helpers.js
================================================
import crypto from 'crypto';
export const _hashLoginToken = (loginToken) => {
var hash = crypto.createHash('sha256');
hash.update(loginToken);
return hash.digest('base64');
};
export const _tokenExpiration = (when) => {
// We pass when through the Date constructor for backwards compatibility;
// `when` used to be a number.
return new Date((new Date(when)).getTime() + _getTokenLifetimeMs());
};
// A large number of expiration days (approximately 100 years worth) that is
// used when creating unexpiring tokens.
const LOGIN_UNEXPIRING_TOKEN_DAYS = 365 * 100;
// how long (in days) until a login token expires
const DEFAULT_LOGIN_EXPIRATION_DAYS = 90;
export const _getTokenLifetimeMs = () => {
// When loginExpirationInDays is set to null, we'll use a really high
// number of days (LOGIN_UNEXPIRABLE_TOKEN_DAYS) to simulate an
// unexpiring token.
const loginExpirationInDays = LOGIN_UNEXPIRING_TOKEN_DAYS;
return (loginExpirationInDays|| DEFAULT_LOGIN_EXPIRATION_DAYS) * 24 * 60 * 60 * 1000;
};
================================================
FILE: packages/vulcan-lib/lib/server/apollo-server/apollo_server.js
================================================
/**
* @see https://www.apollographql.com/docs/apollo-server/whats-new.html
* @see https://www.apollographql.com/docs/apollo-server/migration-two-dot.html
*/
// Meteor WebApp use a Connect server, so we need to
// use apollo-server-express integration
// We also add Express to WebApp in order to use any kind of middlewares
import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import { Meteor } from 'meteor/meteor';
import { WebApp } from 'meteor/webapp';
import _get from 'lodash/get';
import { bodyParserGraphQL } from 'body-parser-graphql';
// import cookiesMiddleware from 'universal-cookie-express';
// import Cookies from 'universal-cookie';
import voyagerMiddleware from 'graphql-voyager/middleware/express';
import getVoyagerConfig from './voyager';
import { graphiqlMiddleware, getGraphiqlConfig } from './graphiql';
import getPlaygroundConfig from './playground';
import initGraphQL from './initGraphQL';
import './settings';
import { engineConfig } from './engine';
import { initContext, computeContextFromReq } from './context.js';
import { GraphQLSchema } from '../graphql/index.js';
import { enableSSR } from '../apollo-ssr';
import universalCookiesMiddleware from 'universal-cookie-express';
import { getApolloApplyMiddlewareOptions, getApolloServerOptions } from './settings';
import { getSetting } from '../../modules/settings.js';
import { formatError } from 'apollo-errors';
import { runCallbacks } from '../../modules/callbacks';
export const setupGraphQLMiddlewares = async (apolloServer, config, apolloApplyMiddlewareOptions) => {
// IMPORTANT: order matters !
// 1 - Add request parsing middleware
// 2 - Add apollo specific middlewares
// 3 - CLOSE CONNEXION (otherwise the endpoint hungs)
// 4 - ONLY THEN you can start adding other middlewares (graphql voyager etc.)
// WebApp.connectHandlers is a connect server
// you can add middlware as usual when using Express/Connect
// Use the Express app instead of just Node connect (allow better middleware chaining)
const app = express();
// parse cookies and assign req.universalCookies object
app.use(universalCookiesMiddleware());
// parse request (order matters)
app.use(
config.path,
// won't handle graphql
//bodyParser.json({ limit: getSetting('apolloServer.jsonParserOptions.limit') })
bodyParserGraphQL({ limit: getSetting('apolloServer.jsonParserOptions.limit') })
);
//WebApp.connectHandlers.use(config.path, bodyParser.text({ type: 'application/graphql' }));
WebApp.connectHandlers.use(app);
// enhance webapp
runCallbacks({
name: 'graphql.middlewares.setup',
iterator: WebApp,
properties: {},
});
await apolloServer.start();
// Provide the Meteor WebApp Connect server instance to Apollo
// Apollo will use it instead of its own HTTP server when handling requests
// For the list of already set middlewares (cookies, compression...), see:
// @see https://github.com/meteor/meteor/blob/master/packages/webapp/webapp_server.js
apolloServer.applyMiddleware({
...apolloApplyMiddlewareOptions,
});
// setup the end point otherwise the request hangs
// TODO: undestand why this is necessary
// @see
WebApp.connectHandlers.use(config.path, (req, res) => {
if (req.method === 'GET') {
res.end();
}
});
};
export const setupToolsMiddlewares = config => {
// Voyager is a GraphQL schema visual explorer
// available on /voyager as a default
WebApp.connectHandlers.use(config.voyagerPath, voyagerMiddleware(getVoyagerConfig(config)));
// Setup GraphiQL
WebApp.connectHandlers.use(config.graphiqlPath, graphiqlMiddleware(getGraphiqlConfig(config)));
};
/**
* setup CORS
* @see https://expressjs.com/en/resources/middleware/cors.html
* @see https://www.apollographql.com/docs/apollo-server/api/apollo-server/#apolloserver
* In Apollo, default cors is defined in packages/apollo-server/src/index.ts, it's too permissive so we use "false" in production
*/
const getCorsOptions = () => {
// enable all cors
const enableAllcors = _get(Meteor.settings, 'apolloServer.corsEnableAll', false);
if (enableAllcors) return true; // will allow all distant queries DANGEROUS
// enable only a whitelist or nothing
const corsWhitelist = _get(Meteor.settings, 'apolloServer.corsWhitelist', []);
const corsOptions =
corsWhitelist && corsWhitelist.length
? {
origin: function(origin, callback) {
if (!origin) {
callback(null, true); // same origin
} else if (corsWhitelist.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error(`Origin ${origin} not allowed by CORS`));
}
},
credentials: true,
}
: process.env.NODE_ENV === 'development'; // default behaviour is activating all in dev, deactivating all in production
return corsOptions;
};
/**
* Options: Apollo server usual options
* Config: a config specific to Vulcan
*/
export const createApolloServer = ({
apolloServerOptions = {}, // apollo options
config, // Vulcan options
}) => {
// given options contains the schema
const apolloServer = new ApolloServer({
// graphql playground (replacement to graphiql), available on the app path
playground: getPlaygroundConfig(config),
// context optionbject or a function of the current request (+ maybe some other params)
debug: Meteor.isDevelopment,
cache: 'bounded',
...apolloServerOptions,
});
// default function does nothing
if (config.configServer) {
config.configServer(apolloServer);
}
return apolloServer;
};
export const onStart = () => {
// Vulcan specific options
const config = {
path: '/graphql',
maxAccountsCacheSizeInMB: 1,
configServer: apolloServer => {},
voyagerPath: '/graphql-voyager',
graphiqlPath: '/graphiql',
// customConfigFromReq
};
const corsOptions = getCorsOptions();
const apolloApplyMiddlewareOptions = {
// @see https://github.com/meteor/meteor/blob/master/packages/webapp/webapp_server.js
// @see https://www.apollographql.com/docs/apollo-server/api/apollo-server.html#Parameters-2
bodyParser: false, // added manually later
path: config.path,
app: WebApp.connectHandlers,
cors: corsOptions,
...getApolloApplyMiddlewareOptions(),
};
// init context
const initialContext = initContext();
// this replace the previous syntax graphqlExpress(async req => { ... })
// this function takes the context, which contains the current request,
// and setup the options accordingly ({req}) => { ...; return options }
const context = computeContextFromReq(initialContext);
// define executableSchema
initGraphQL();
// create server
const apolloServer = createApolloServer({
config,
apolloServerOptions: {
engine: engineConfig,
schema: GraphQLSchema.executableSchema,
formatError,
tracing: getSetting('apolloTracing', Meteor.isDevelopment),
cacheControl: {
defaultMaxAge: 1000,
},
context: ({ req }) => context(req),
...getApolloServerOptions(),
},
});
// NOTE: order matters here
// /graphql middlewares (request parsing)
setupGraphQLMiddlewares(apolloServer, config, apolloApplyMiddlewareOptions);
//// other middlewares (dev tools etc.)
if (Meteor.isDevelopment) {
setupToolsMiddlewares(config);
}
// ssr
const disableSSR = getSetting('apolloSsr.disable', false);
if (!disableSSR) {
enableSSR({ computeContext: context });
}
return apolloServer;
};
================================================
FILE: packages/vulcan-lib/lib/server/apollo-server/context.js
================================================
/**
* Context prop of the ApolloServer config
*
* It sets up the server options based on the current request
* Replacement to the syntax graphqlExpress(async req => {... })
* Current pattern:
* @see https://www.apollographql.com/docs/apollo-server/migration-two-dot.html#request-headers
* @see https://github.com/apollographql/apollo-server/issues/1066
* Previous implementation:
* @see https://github.com/apollographql/apollo-server/issues/420
*/
//import deepmerge from 'deepmerge';
import DataLoader from 'dataloader';
import { Collections } from '../../modules/collections.js';
import { runCallbacks } from '../../modules/callbacks.js';
import findByIds from '../../modules/findbyids.js';
import { GraphQLSchema } from '../graphql/index.js';
import _merge from 'lodash/merge';
import { getHeaderLocale } from '../intl.js';
import { getLocale } from '../../modules/intl.js';
import { getSetting } from '../../modules/settings.js';
import { WebApp } from 'meteor/webapp';
import { Accounts } from 'meteor/accounts-base';
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
/**
* Called once on server creation
* @param {*} currentContext
*/
export const initContext = currentContext => {
let context;
if (currentContext) {
context = { ...currentContext };
} else {
context = {};
}
// add all collections to context
Collections.forEach(c => (context[c.collectionName] = c));
// merge with custom context
// TODO: deepmerge created an infinite loop here
context = _merge({}, context, GraphQLSchema.context);
return context;
};
import Cookies from 'universal-cookie';
// initial request will get the login token from a cookie, subsequent requests from
// the header
export const getAuthToken = req => {
return req.headers.authorization || new Cookies(req.cookies).get('meteor_login_token');
};
const getUser = async loginToken => {
if (loginToken) {
check(loginToken, String)
const hashedToken = Accounts._hashLoginToken(loginToken)
const user = await Meteor.users.rawCollection().findOne({
'services.resume.loginTokens.hashedToken': hashedToken
})
if (user) {
// find the right login token corresponding, the current user may have
// several sessions logged on different browsers / computers
const tokenInformation = user.services.resume.loginTokens.find(
tokenInfo => tokenInfo.hashedToken === hashedToken
)
const expiresAt = Accounts._tokenExpiration(tokenInformation.when)
const isExpired = expiresAt < new Date()
if (!isExpired) {
return user
}
}
}
}
// @see https://www.apollographql.com/docs/react/recipes/meteor#Server
export const setupAuthToken = async (context, req) => {
const authToken = getAuthToken(req);
const user = await getUser(authToken);
if (user) {
context.userId = user._id;
context.currentUser = user;
// Not useful
//context.authToken = authToken;
// identify user to any server-side analytics providers
runCallbacks('events.identify', user);
} else {
context.userId = undefined;
context.currentUser = undefined;
}
};
// @see https://github.com/facebook/dataloader#caching-per-request
const generateDataLoaders = context => {
// go over context and add Dataloader to each collection
Collections.forEach(collection => {
context[collection.options.collectionName].loader = new DataLoader(ids => findByIds(collection, ids, context), {
cache: true,
});
});
return context;
};
// Returns a function called on every request to compute context
export const computeContextFromReq = (currentContext, customContextFromReq) => {
// givenOptions can be either a function of the request or an object
const getBaseContext = req => (customContextFromReq ? { ...currentContext, ...customContextFromReq(req) } : { ...currentContext });
// create options given the current request
const handleReq = async req => {
const { headers } = req;
let context;
// eslint-disable-next-line no-unused-vars
let user = null;
context = getBaseContext(req);
generateDataLoaders(context);
// note: custom default resolver doesn't currently work
// see https://github.com/apollographql/apollo-server/issues/716
// @options.fieldResolver = (source, args, context, info) => {
// return source[info.fieldName];
// }
await setupAuthToken(context, req);
//add the headers to the context
context.headers = headers;
// pass the whole req for advanced usage, like fetching IP from connection
context.req = req;
// if apiKey is present, assign "fake" currentUser with admin rights
if (headers.apikey && headers.apikey === getSetting('vulcan.apiKey')) {
context.currentUser = { isAdmin: true, isApiUser: true };
}
context.locale = getHeaderLocale(headers, context.currentUser && context.currentUser.locale);
const locale = getLocale(context.locale);
// see https://forums.meteor.com/t/can-i-edit-html-tag-in-meteor/5867/7
WebApp.addHtmlAttributeHook(function() {
let htmlAttributes = {
lang: context.locale
};
if (locale?.rtl === true) {
htmlAttributes.class = 'rtl';
} else {
htmlAttributes.class = 'ltr';
}
return htmlAttributes;
});
context = await runCallbacks({ name: 'graphql.context', iterator: context });
return context;
};
return handleReq;
};
================================================
FILE: packages/vulcan-lib/lib/server/apollo-server/engine.js
================================================
import { getSetting } from '../../modules/settings.js';
// @see https://www.apollographql.com/docs/apollo-server/api/apollo-server.html#EngineReportingOptions
let engineConfigObject = getSetting('apolloEngine');
if (!engineConfigObject || !engineConfigObject.apiKey) {
engineConfigObject = {
apiKey: process.env.ENGINE_API_KEY,
schemaTag: process.env.ENGINE_SCHEMA_TAG
};
}
export const engineConfig = engineConfigObject && engineConfigObject.apiKey ? engineConfigObject : undefined;
================================================
FILE: packages/vulcan-lib/lib/server/apollo-server/graphiql.js
================================================
export const getGraphiqlConfig = currentConfig => ({
endpointURL: currentConfig.path,
passHeader: "'Authorization': localStorage['Meteor.loginToken']", // eslint-disable-line quotes
});
// LEGACY SUPPORT FOR GRAPHIQL
// Code is taken from apollo 1.4 code and
// @see https://github.com/eritikass/express-graphiql-middleware
// This is the only way to get graphiql to work
import url from 'url';
// @seehttps://github.com/apollographql/apollo-server/blob/v1.4.0/packages/apollo-server-module-graphiql/src/resolveGraphiQLString.ts
// renderGraphiQL
/*
* Mostly taken straight from express-graphql, so see their licence
* (https://github.com/graphql/express-graphql/blob/master/LICENSE)
*/
/*
* Arguments:
*
* - endpointURL: the relative or absolute URL for the endpoint which GraphiQL will make queries to
* - (optional) query: the GraphQL query to pre-fill in the GraphiQL UI
* - (optional) variables: a JS object of variables to pre-fill in the GraphiQL UI
* - (optional) operationName: the operationName to pre-fill in the GraphiQL UI
* - (optional) result: the result of the query to pre-fill in the GraphiQL UI
* - (optional) passHeader: a string that will be added to the header object.
* For example "'Authorization': localStorage['Meteor.loginToken']" for meteor
* - (optional) editorTheme: a CodeMirror theme to be applied to the GraphiQL UI
* - (optional) websocketConnectionParams: an object to pass to the web socket server
*/
// Current latest version of GraphiQL.
const GRAPHIQL_VERSION = '0.11.11';
const SUBSCRIPTIONS_TRANSPORT_VERSION = '0.9.9';
// Ensures string values are safe to be used within a
${
usingEditorTheme
? ``
: ''
}
${usingHttp ? '' : ''}
${
usingWs
? ``
: ''
}
${
usingWs && usingHttp
? ''
: ''
}